diff --git a/Gemfile b/Gemfile index baaddab2e..e9af29d55 100644 --- a/Gemfile +++ b/Gemfile @@ -62,6 +62,8 @@ gem "possessive" # Strip whitespace from active record attributes gem "auto_strip_attributes" # Use sidekiq for background processing +gem "factory_bot_rails" +gem "faker" gem "method_source", "~> 1.1" gem "rails_admin", "~> 3.1" gem "ruby-openai" @@ -75,8 +77,6 @@ group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem "byebug", platforms: %i[mri mingw x64_mingw] gem "dotenv-rails" - gem "factory_bot_rails" - gem "faker" gem "pry-byebug" gem "parallel_tests" diff --git a/app/components/create_log_actions_component.html.erb b/app/components/create_log_actions_component.html.erb index 53e2bb57b..a600f3290 100644 --- a/app/components/create_log_actions_component.html.erb +++ b/app/components/create_log_actions_component.html.erb @@ -7,5 +7,10 @@ <% if user.support? %> <%= govuk_button_link_to view_uploads_button_copy, view_uploads_button_href, secondary: true %> <% end %> + + <% if FeatureToggle.create_test_logs_enabled? %> + <%= govuk_button_link_to "Create test log", create_test_log_href, secondary: true %> + <%= govuk_button_link_to "Create test log (setup only)", create_setup_test_log_href, secondary: true %> + <% end %> <% end %> diff --git a/app/components/create_log_actions_component.rb b/app/components/create_log_actions_component.rb index 4395c48a9..896bfe97e 100644 --- a/app/components/create_log_actions_component.rb +++ b/app/components/create_log_actions_component.rb @@ -34,6 +34,14 @@ class CreateLogActionsComponent < ViewComponent::Base send("bulk_upload_#{log_type}_log_path", id: "start") end + def create_test_log_href + send("create_test_#{log_type}_log_path") + end + + def create_setup_test_log_href + send("create_setup_test_#{log_type}_log_path") + end + def view_uploads_button_copy "View #{log_type} bulk uploads" end diff --git a/app/controllers/csv_downloads_controller.rb b/app/controllers/csv_downloads_controller.rb new file mode 100644 index 000000000..25f70026f --- /dev/null +++ b/app/controllers/csv_downloads_controller.rb @@ -0,0 +1,27 @@ +class CsvDownloadsController < ApplicationController + before_action :authenticate_user! + + def show + @csv_download = CsvDownload.find(params[:id]) + authorize @csv_download + + return render "errors/download_link_expired" if @csv_download.expired? + end + + def download + csv_download = CsvDownload.find(params[:id]) + authorize csv_download + + return render "errors/download_link_expired" if csv_download.expired? + + downloader = Csv::Downloader.new(csv_download:) + + if Rails.env.development? + downloader.call + send_file downloader.path, filename: csv_download.filename, type: "text/csv" + else + presigned_url = downloader.presigned_url + redirect_to presigned_url, allow_other_host: true + end + end +end diff --git a/app/controllers/lettings_logs_controller.rb b/app/controllers/lettings_logs_controller.rb index cc3c731d5..af3a6c32f 100644 --- a/app/controllers/lettings_logs_controller.rb +++ b/app/controllers/lettings_logs_controller.rb @@ -149,6 +149,20 @@ class LettingsLogsController < LogsController end end + def create_test_log + return render_not_found unless FeatureToggle.create_test_logs_enabled? + + log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA") + redirect_to lettings_log_path(log) + end + + def create_setup_test_log + return render_not_found unless FeatureToggle.create_test_logs_enabled? + + log = FactoryBot.create(:lettings_log, :setup_completed, assigned_to: current_user) + redirect_to lettings_log_path(log) + end + private def session_filters diff --git a/app/controllers/merge_requests_controller.rb b/app/controllers/merge_requests_controller.rb index a21d42bbb..e38d1bdf0 100644 --- a/app/controllers/merge_requests_controller.rb +++ b/app/controllers/merge_requests_controller.rb @@ -116,6 +116,7 @@ private def merge_request_params merge_params = params.fetch(:merge_request, {}).permit( :requesting_organisation_id, + :has_helpdesk_ticket, :helpdesk_ticket, :status, :absorbing_organisation_id, @@ -124,6 +125,7 @@ private ) merge_params[:requesting_organisation_id] = current_user.organisation.id + merge_params[:helpdesk_ticket] = nil if merge_params[:has_helpdesk_ticket] == "false" merge_params end @@ -143,6 +145,7 @@ private if [day, month, year].none?(&:blank?) && Date.valid_date?(year.to_i, month.to_i, day.to_i) merge_request_params["merge_date"] = Time.zone.local(year.to_i, month.to_i, day.to_i) + @merge_request.errors.add(:merge_date, :more_than_year_from_today) if Time.zone.local(year.to_i, month.to_i, day.to_i) - 1.year > Time.zone.today else @merge_request.errors.add(:merge_date, :invalid) end @@ -150,6 +153,14 @@ private if merge_request_params[:existing_absorbing_organisation].nil? @merge_request.errors.add(:existing_absorbing_organisation, :blank) end + when "helpdesk_ticket" + @merge_request.has_helpdesk_ticket = merge_request_params[:has_helpdesk_ticket] + @merge_request.helpdesk_ticket = merge_request_params[:helpdesk_ticket] + if merge_request_params[:has_helpdesk_ticket].blank? + @merge_request.errors.add(:has_helpdesk_ticket, :blank) + elsif merge_request_params[:has_helpdesk_ticket] == "true" && merge_request_params[:helpdesk_ticket].blank? + @merge_request.errors.add(:helpdesk_ticket, :blank) + end end end diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 61cd43674..8ffe426d7 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -24,7 +24,7 @@ class OrganisationsController < ApplicationController end def schemes - organisation_schemes = Scheme.visible.where(owning_organisation: [@organisation] + @organisation.parent_organisations) + organisation_schemes = Scheme.visible.where(owning_organisation: [@organisation] + @organisation.parent_organisations + @organisation.absorbed_organisations.visible.merged_during_open_collection_period) @pagy, @schemes = pagy(filter_manager.filtered_schemes(organisation_schemes, search_term, session_filters)) @searched = search_term.presence diff --git a/app/controllers/sales_logs_controller.rb b/app/controllers/sales_logs_controller.rb index af9879896..8799fe528 100644 --- a/app/controllers/sales_logs_controller.rb +++ b/app/controllers/sales_logs_controller.rb @@ -119,6 +119,20 @@ class SalesLogsController < LogsController end end + def create_test_log + return render_not_found unless FeatureToggle.create_test_logs_enabled? + + log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user) + redirect_to sales_log_path(log) + end + + def create_setup_test_log + return render_not_found unless FeatureToggle.create_test_logs_enabled? + + log = FactoryBot.create(:sales_log, :shared_ownership_setup_complete, assigned_to: current_user) + redirect_to sales_log_path(log) + end + private def session_filters diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 3df38a237..3dc642345 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -118,6 +118,10 @@ class SchemesController < ApplicationController validation_errors scheme_params if @scheme.errors.empty? && @scheme.save + if @scheme.owning_organisation.merge_date.present? + deactivation = SchemeDeactivationPeriod.new(scheme: @scheme, deactivation_date: @scheme.owning_organisation.merge_date) + deactivation.save!(validate: false) + end redirect_to scheme_primary_client_group_path(@scheme) else if @scheme.errors.any? { |error| error.attribute == :owning_organisation } @@ -152,7 +156,7 @@ class SchemesController < ApplicationController flash[:notice] = if scheme_previously_confirmed "#{@scheme.service_name} has been updated." else - "#{@scheme.service_name} has been created. It does not require helpdesk approval." + "#{@scheme.service_name} has been created." end redirect_to scheme_path(@scheme) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f27bfc2b3..57036cabe 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -114,6 +114,7 @@ class UsersController < ApplicationController validate_attributes if @user.errors.empty? && @user.save + flash[:notice] = "Invitation sent to #{@user.email}" redirect_to created_user_redirect_path else unless @user.errors[:organisation].empty? diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 3a4c337ea..99c69c636 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -192,9 +192,15 @@ module FiltersHelper end def show_scheme_managing_org_filter?(user) + return true if user.support? + org = user.organisation + stock_owners = org.stock_owners.count + recently_absorbed_with_stock = org.absorbed_organisations.visible.merged_during_open_collection_period.where(holds_own_stock: true).count + + relevant_orgs_count = stock_owners + recently_absorbed_with_stock + (org.holds_own_stock? ? 1 : 0) - user.support? || org.stock_owners.count > 1 || (org.holds_own_stock? && org.stock_owners.count.positive?) + relevant_orgs_count > 1 end def logs_for_both_needstypes_present?(organisation) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 6283ef42e..a8ed72120 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -9,7 +9,7 @@ module MergeRequestsHelper def request_details(merge_request) [ { label: "Requester", value: display_value_or_placeholder(merge_request.requester&.name) }, - { label: "Helpdesk ticket", value: merge_request.helpdesk_ticket.present? ? link_to("#{merge_request.helpdesk_ticket} (opens in a new tab)", "https://mhclgdigital.atlassian.net/browse/#{merge_request.helpdesk_ticket}", target: "_blank", rel: "noopener noreferrer") : display_value_or_placeholder(nil), action: merge_request_action(merge_request, "helpdesk_ticket") }, + { label: "Helpdesk ticket", value: helpdesk_ticket_value(merge_request), action: merge_request_action(merge_request, "helpdesk_ticket") }, { label: "Status", value: status_tag(merge_request.status) }, ] end @@ -276,4 +276,18 @@ module MergeRequestsHelper def any_organisations_share_logs?(organisations, type) organisations.any? { |organisation| organisation.send("#{type}_logs").filter_by_managing_organisation(organisations.where.not(id: organisation.id)).exists? } end + + def begin_merge_disabled?(merge_request) + merge_request.status != "ready_to_merge" || merge_request.merge_date.future? + end + + def helpdesk_ticket_value(merge_request) + if merge_request.helpdesk_ticket.present? + link_to("#{merge_request.helpdesk_ticket} (opens in a new tab)", "https://mhclgdigital.atlassian.net/browse/#{merge_request.helpdesk_ticket}", target: "_blank", rel: "noopener noreferrer") + elsif merge_request.has_helpdesk_ticket == false + "Not reported by a helpdesk ticket" + else + display_value_or_placeholder(nil) + end + end end diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index bcd40b082..12d86aba8 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -20,11 +20,14 @@ module SchemesHelper end def owning_organisation_options(current_user) - all_orgs = Organisation.visible.map { |org| OpenStruct.new(id: org.id, name: org.name) } - user_org = [OpenStruct.new(id: current_user.organisation_id, name: current_user.organisation.name)] - stock_owners = current_user.organisation.stock_owners.visible.map { |org| OpenStruct.new(id: org.id, name: org.name) } - merged_organisations = current_user.organisation.absorbed_organisations.visible.merged_during_open_collection_period.map { |org| OpenStruct.new(id: org.id, name: org.name) } - current_user.support? ? all_orgs : user_org + stock_owners + merged_organisations + if current_user.support? + Organisation.visible.map { |org| OpenStruct.new(id: org.id, name: org.name) } + else + user_org = [current_user.organisation] + stock_owners = current_user.organisation.stock_owners.visible.filter { |org| org.status == :active || (org.status == :merged && org.merge_date >= FormHandler.instance.start_date_of_earliest_open_for_editing_collection_period) } + merged_organisations = current_user.organisation.absorbed_organisations.visible.merged_during_open_collection_period + (user_org + stock_owners + merged_organisations).map { |org| OpenStruct.new(id: org.id, name: org.name) } + end end def null_option @@ -81,7 +84,12 @@ module SchemesHelper when :deactivating_soon "This scheme deactivates on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date." when :deactivated - "This scheme deactivated on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date." + case scheme.owning_organisation.status + when :active + "This scheme deactivated on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date." + when :merged + "This scheme has been deactivated due to #{scheme.owning_organisation.name} merging into #{scheme.owning_organisation.absorbing_organisation.name} on #{scheme.owning_organisation.merge_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Use the after merge organisation for schemes and locations active after this date." + end 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/jobs/email_csv_job.rb b/app/jobs/email_csv_job.rb index 58f2d50b8..dd0f2917c 100644 --- a/app/jobs/email_csv_job.rb +++ b/app/jobs/email_csv_job.rb @@ -1,9 +1,10 @@ class EmailCsvJob < ApplicationJob + include Rails.application.routes.url_helpers queue_as :default BYTE_ORDER_MARK = "\uFEFF".freeze # Required to ensure Excel always reads CSV as UTF-8 - EXPIRATION_TIME = 24.hours.to_i + EXPIRATION_TIME = 48.hours.to_i def perform(user, search_term = nil, filters = {}, all_orgs = false, organisation = nil, codes_only_export = false, log_type = "lettings", year = nil) # rubocop:disable Style/OptionalBooleanParameter - sidekiq can't serialise named params export_type = codes_only_export ? "codes" : "labels" @@ -20,10 +21,16 @@ class EmailCsvJob < ApplicationJob filename = "#{[log_type, 'logs', organisation&.name, Time.zone.now].compact.join('-')}.csv" - storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + storage_service = if FeatureToggle.upload_enabled? + Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + else + Storage::LocalDiskService.new + end + storage_service.write_file(filename, BYTE_ORDER_MARK + csv_string) + csv_download = CsvDownload.create!(user:, organisation: user.organisation, filename:, download_type: log_type, expiration_time: EXPIRATION_TIME) - url = storage_service.get_presigned_url(filename, EXPIRATION_TIME) + url = csv_download_url(csv_download.id, host: ENV["APP_HOST"]) CsvDownloadMailer.new.send_csv_download_mail(user, url, EXPIRATION_TIME) end diff --git a/app/jobs/scheme_email_csv_job.rb b/app/jobs/scheme_email_csv_job.rb index 44d016a90..803d3dce3 100644 --- a/app/jobs/scheme_email_csv_job.rb +++ b/app/jobs/scheme_email_csv_job.rb @@ -1,9 +1,10 @@ class SchemeEmailCsvJob < ApplicationJob + include Rails.application.routes.url_helpers queue_as :default BYTE_ORDER_MARK = "\uFEFF".freeze # Required to ensure Excel always reads CSV as UTF-8 - EXPIRATION_TIME = 24.hours.to_i + EXPIRATION_TIME = 48.hours.to_i def perform(user, search_term = nil, filters = {}, all_orgs = false, organisation = nil, download_type = "combined") # rubocop:disable Style/OptionalBooleanParameter - sidekiq can't serialise named params unfiltered_schemes = if organisation.present? && user.support? @@ -23,10 +24,16 @@ class SchemeEmailCsvJob < ApplicationJob filename = "#{['schemes-and-locations', organisation&.name, Time.zone.now].compact.join('-')}.csv" end - storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + storage_service = if FeatureToggle.upload_enabled? + Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + else + Storage::LocalDiskService.new + end + storage_service.write_file(filename, BYTE_ORDER_MARK + csv_string) + csv_download = CsvDownload.create!(user:, organisation: user.organisation, filename:, download_type:, expiration_time: EXPIRATION_TIME) - url = storage_service.get_presigned_url(filename, EXPIRATION_TIME) + url = csv_download_url(csv_download.id, host: ENV["APP_HOST"]) CsvDownloadMailer.new.send_csv_download_mail(user, url, EXPIRATION_TIME) end diff --git a/app/models/csv_download.rb b/app/models/csv_download.rb new file mode 100644 index 000000000..4064c62f3 --- /dev/null +++ b/app/models/csv_download.rb @@ -0,0 +1,10 @@ +class CsvDownload < ApplicationRecord + enum download_type: { lettings: "lettings", sales: "sales", schemes: "schemes", locations: "locations", combined: "combined" } + + belongs_to :user + belongs_to :organisation + + def expired? + created_at < expiration_time.seconds.ago + end +end diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 9219392f7..4692a0e6b 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -124,6 +124,11 @@ module DerivedVariables::LettingsLogVariables self.nationality_all = nationality_all_group if nationality_uk_or_prefers_not_to_say? + if startdate_changed? && !LocalAuthority.active(startdate).where(code: la).exists? + self.la = nil + self.is_la_inferred = false + end + reset_address_fields! if is_supported_housing? end diff --git a/app/models/derived_variables/sales_log_variables.rb b/app/models/derived_variables/sales_log_variables.rb index c3edcd072..d9af88e65 100644 --- a/app/models/derived_variables/sales_log_variables.rb +++ b/app/models/derived_variables/sales_log_variables.rb @@ -75,6 +75,14 @@ module DerivedVariables::SalesLogVariables self.nationality_all = nationality_all_group if nationality_uk_or_prefers_not_to_say? self.nationality_all_buyer2 = nationality_all_buyer2_group if nationality2_uk_or_prefers_not_to_say? + if saledate_changed? && !LocalAuthority.active(saledate).where(code: la).exists? + self.la = nil + 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/lettings/subsections/household_needs.rb b/app/models/form/lettings/subsections/household_needs.rb index 3bfbbb336..2f6900f4f 100644 --- a/app/models/form/lettings/subsections/household_needs.rb +++ b/app/models/form/lettings/subsections/household_needs.rb @@ -2,7 +2,6 @@ class Form::Lettings::Subsections::HouseholdNeeds < ::Form::Subsection def initialize(id, hsh, section) super @id = "household_needs" - @copy_key = "lettings.household_needs.housingneeds_type" @label = "Household needs" @depends_on = [{ "non_location_setup_questions_completed?" => true }] end diff --git a/app/models/form/page.rb b/app/models/form/page.rb index 7a5c4bf87..c1c09c362 100644 --- a/app/models/form/page.rb +++ b/app/models/form/page.rb @@ -25,7 +25,7 @@ class Form::Page delegate :form, to: :subsection def copy_key - @copy_key ||= "#{form.type}.#{subsection.id}.#{questions[0].id}" + @copy_key ||= "#{form.type}.#{subsection.copy_key}.#{questions[0].id}" end def header diff --git a/app/models/form/question.rb b/app/models/form/question.rb index 9409350f6..05eec89dd 100644 --- a/app/models/form/question.rb +++ b/app/models/form/question.rb @@ -51,7 +51,7 @@ class Form::Question delegate :form, to: :subsection def copy_key - @copy_key ||= "#{form.type}.#{subsection.id}.#{id}" + @copy_key ||= "#{form.type}.#{subsection.copy_key}.#{id}" end def check_answer_label 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/buyer1_income_max_value_check.rb b/app/models/form/sales/pages/buyer1_income_discounted_max_value_check.rb similarity index 78% rename from app/models/form/sales/pages/buyer1_income_max_value_check.rb rename to app/models/form/sales/pages/buyer1_income_discounted_max_value_check.rb index 55599ff26..6a774d36f 100644 --- a/app/models/form/sales/pages/buyer1_income_max_value_check.rb +++ b/app/models/form/sales/pages/buyer1_income_discounted_max_value_check.rb @@ -1,12 +1,12 @@ -class Form::Sales::Pages::Buyer1IncomeMaxValueCheck < ::Form::Page +class Form::Sales::Pages::Buyer1IncomeDiscountedMaxValueCheck < ::Form::Page def initialize(id, hsh, subsection, check_answers_card_number:) super(id, hsh, subsection) @depends_on = [ { - "income1_over_soft_max?" => true, + "income1_over_soft_max_for_discounted_ownership?" => true, }, ] - @copy_key = "sales.soft_validations.income1_value_check.max" + @copy_key = "sales.soft_validations.income1_value_check.discounted" @title_text = { "translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text", "arguments" => [ diff --git a/app/models/form/sales/pages/buyer1_income_min_value_check.rb b/app/models/form/sales/pages/buyer1_income_ecstat_value_check.rb similarity index 69% rename from app/models/form/sales/pages/buyer1_income_min_value_check.rb rename to app/models/form/sales/pages/buyer1_income_ecstat_value_check.rb index 9fc85bb76..34b408432 100644 --- a/app/models/form/sales/pages/buyer1_income_min_value_check.rb +++ b/app/models/form/sales/pages/buyer1_income_ecstat_value_check.rb @@ -1,12 +1,12 @@ -class Form::Sales::Pages::Buyer1IncomeMinValueCheck < ::Form::Page +class Form::Sales::Pages::Buyer1IncomeEcstatValueCheck < ::Form::Page def initialize(id, hsh, subsection) super @depends_on = [ { - "income1_under_soft_min?" => true, + "income1_outside_soft_range_for_ecstat?" => true, }, ] - @copy_key = "sales.soft_validations.income1_value_check.min" + @copy_key = "sales.soft_validations.income1_value_check.ecstat" @title_text = { "translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text", "arguments" => [ @@ -15,16 +15,16 @@ class Form::Sales::Pages::Buyer1IncomeMinValueCheck < ::Form::Page "arguments_for_key" => "income1", "i18n_template" => "income", }, - { - "key" => "income_soft_min_for_ecstat", - "arguments_for_key" => "ecstat1", - "i18n_template" => "minimum", - }, ], } @informative_text = { "translation" => "forms.#{form.start_date.year}.#{@copy_key}.informative_text", - "arguments" => [], + "arguments" => [ + { + "key" => "income1_more_or_less_text", + "i18n_template" => "more_or_less", + }, + ], } end diff --git a/app/models/form/sales/pages/buyer2_income_max_value_check.rb b/app/models/form/sales/pages/buyer2_income_discounted_max_value_check.rb similarity index 78% rename from app/models/form/sales/pages/buyer2_income_max_value_check.rb rename to app/models/form/sales/pages/buyer2_income_discounted_max_value_check.rb index deece885e..356a5ed20 100644 --- a/app/models/form/sales/pages/buyer2_income_max_value_check.rb +++ b/app/models/form/sales/pages/buyer2_income_discounted_max_value_check.rb @@ -1,12 +1,12 @@ -class Form::Sales::Pages::Buyer2IncomeMaxValueCheck < ::Form::Page +class Form::Sales::Pages::Buyer2IncomeDiscountedMaxValueCheck < ::Form::Page def initialize(id, hsh, subsection, check_answers_card_number:) super(id, hsh, subsection) @depends_on = [ { - "income2_over_soft_max?" => true, + "income2_over_soft_max_for_discounted_ownership?" => true, }, ] - @copy_key = "sales.soft_validations.income2_value_check.max" + @copy_key = "sales.soft_validations.income2_value_check.discounted" @title_text = { "translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text", "arguments" => [ diff --git a/app/models/form/sales/pages/buyer2_income_min_value_check.rb b/app/models/form/sales/pages/buyer2_income_ecstat_value_check.rb similarity index 69% rename from app/models/form/sales/pages/buyer2_income_min_value_check.rb rename to app/models/form/sales/pages/buyer2_income_ecstat_value_check.rb index a7b68cd10..3b9503669 100644 --- a/app/models/form/sales/pages/buyer2_income_min_value_check.rb +++ b/app/models/form/sales/pages/buyer2_income_ecstat_value_check.rb @@ -1,12 +1,12 @@ -class Form::Sales::Pages::Buyer2IncomeMinValueCheck < ::Form::Page +class Form::Sales::Pages::Buyer2IncomeEcstatValueCheck < ::Form::Page def initialize(id, hsh, subsection) super @depends_on = [ { - "income2_under_soft_min?" => true, + "income2_outside_soft_range_for_ecstat?" => true, }, ] - @copy_key = "sales.soft_validations.income2_value_check.min" + @copy_key = "sales.soft_validations.income2_value_check.ecstat" @title_text = { "translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text", "arguments" => [ @@ -15,16 +15,16 @@ class Form::Sales::Pages::Buyer2IncomeMinValueCheck < ::Form::Page "arguments_for_key" => "income2", "i18n_template" => "income", }, - { - "key" => "income_soft_min_for_ecstat", - "arguments_for_key" => "ecstat2", - "i18n_template" => "minimum", - }, ], } @informative_text = { "translation" => "forms.#{form.start_date.year}.#{@copy_key}.informative_text", - "arguments" => [], + "arguments" => [ + { + "key" => "income2_more_or_less_text", + "i18n_template" => "more_or_less", + }, + ], } end diff --git a/app/models/form/sales/pages/buyer_interview.rb b/app/models/form/sales/pages/buyer_interview.rb index c6a43690b..4398e434a 100644 --- a/app/models/form/sales/pages/buyer_interview.rb +++ b/app/models/form/sales/pages/buyer_interview.rb @@ -2,7 +2,7 @@ class Form::Sales::Pages::BuyerInterview < ::Form::Page def initialize(id, hsh, subsection, joint_purchase:) super(id, hsh, subsection) @joint_purchase = joint_purchase - @copy_key = "sales.#{subsection.id}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" + @copy_key = "sales.#{subsection.copy_key}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" end def questions diff --git a/app/models/form/sales/pages/combined_income_max_value_check.rb b/app/models/form/sales/pages/combined_income_max_value_check.rb index 1cd1fd851..89eea3027 100644 --- a/app/models/form/sales/pages/combined_income_max_value_check.rb +++ b/app/models/form/sales/pages/combined_income_max_value_check.rb @@ -3,7 +3,7 @@ class Form::Sales::Pages::CombinedIncomeMaxValueCheck < ::Form::Page super(id, hsh, subsection) @depends_on = [ { - "combined_income_over_soft_max?" => true, + "combined_income_over_soft_max_for_discounted_ownership?" => true, }, ] @copy_key = "sales.soft_validations.combined_income_value_check" diff --git a/app/models/form/sales/pages/deposit.rb b/app/models/form/sales/pages/deposit.rb index 4870a3c35..b204227b2 100644 --- a/app/models/form/sales/pages/deposit.rb +++ b/app/models/form/sales/pages/deposit.rb @@ -3,7 +3,6 @@ class Form::Sales::Pages::Deposit < ::Form::Page super(id, hsh, subsection) @ownershipsch = ownershipsch @optional = optional - @copy_key = "sales.sale_information.deposit" end def questions diff --git a/app/models/form/sales/pages/deposit_discount.rb b/app/models/form/sales/pages/deposit_discount.rb index 84fcbb45f..3fae9c0f8 100644 --- a/app/models/form/sales/pages/deposit_discount.rb +++ b/app/models/form/sales/pages/deposit_discount.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::DepositDiscount < ::Form::Page def initialize(id, hsh, subsection, optional:) super(id, hsh, subsection) @optional = optional - @copy_key = "sales.sale_information.cashdis" end def questions diff --git a/app/models/form/sales/pages/discount.rb b/app/models/form/sales/pages/discount.rb index 2d632985e..38d675a77 100644 --- a/app/models/form/sales/pages/discount.rb +++ b/app/models/form/sales/pages/discount.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::Discount < ::Form::Page def initialize(id, hsh, subsection) super @id = "discount" - @copy_key = "sales.sale_information.discount" @depends_on = [{ "right_to_buy?" => true, }] diff --git a/app/models/form/sales/pages/equity.rb b/app/models/form/sales/pages/equity.rb index 9bf3050a0..12d3c0a1b 100644 --- a/app/models/form/sales/pages/equity.rb +++ b/app/models/form/sales/pages/equity.rb @@ -1,7 +1,6 @@ class Form::Sales::Pages::Equity < ::Form::Page def initialize(id, hsh, subsection) super - @id = "equity" @copy_key = "sales.sale_information.equity" end diff --git a/app/models/form/sales/pages/estate_management_fee.rb b/app/models/form/sales/pages/estate_management_fee.rb new file mode 100644 index 000000000..5be478f80 --- /dev/null +++ b/app/models/form/sales/pages/estate_management_fee.rb @@ -0,0 +1,13 @@ +class Form::Sales::Pages::EstateManagementFee < ::Form::Page + def initialize(id, hsh, subsection) + super + @copy_key = "sales.sale_information.management_fee" + end + + def questions + @questions ||= [ + Form::Sales::Questions::HasManagementFee.new(nil, nil, self), + Form::Sales::Questions::ManagementFee.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/pages/extra_borrowing.rb b/app/models/form/sales/pages/extra_borrowing.rb index c6ddb705a..d86db67b5 100644 --- a/app/models/form/sales/pages/extra_borrowing.rb +++ b/app/models/form/sales/pages/extra_borrowing.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::ExtraBorrowing < ::Form::Page def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @ownershipsch = ownershipsch - @copy_key = "sales.sale_information.extrabor" @description = "" @subsection = subsection @depends_on = [{ diff --git a/app/models/form/sales/pages/grant.rb b/app/models/form/sales/pages/grant.rb index 2f96701c5..1d11fba82 100644 --- a/app/models/form/sales/pages/grant.rb +++ b/app/models/form/sales/pages/grant.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::Grant < ::Form::Page def initialize(id, hsh, subsection) super @id = "grant" - @copy_key = "sales.sale_information.grant" @depends_on = [{ "right_to_buy?" => false, "rent_to_buy_full_ownership?" => false, diff --git a/app/models/form/sales/pages/living_before_purchase.rb b/app/models/form/sales/pages/living_before_purchase.rb index 3bb5510ce..b8797537b 100644 --- a/app/models/form/sales/pages/living_before_purchase.rb +++ b/app/models/form/sales/pages/living_before_purchase.rb @@ -19,11 +19,17 @@ class Form::Sales::Pages::LivingBeforePurchase < ::Form::Page end end - def depends_on + def routed_to?(log, _user) + super && page_routed_to?(log) + end + + def page_routed_to?(log) + return false if form.start_year_2025_or_later? && log.resale != 2 + if @joint_purchase - [{ "joint_purchase?" => true }] + log.joint_purchase? else - [{ "not_joint_purchase?" => true }, { "jointpur" => nil }] + log.not_joint_purchase? || log.jointpur.nil? end end end diff --git a/app/models/form/sales/pages/monthly_rent.rb b/app/models/form/sales/pages/monthly_rent.rb index 943e47cff..29f0d895f 100644 --- a/app/models/form/sales/pages/monthly_rent.rb +++ b/app/models/form/sales/pages/monthly_rent.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::MonthlyRent < ::Form::Page def initialize(id, hsh, subsection) super @id = "monthly_rent" - @copy_key = "sales.sale_information.mrent" 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/mortgage_amount.rb b/app/models/form/sales/pages/mortgage_amount.rb index 41fba167c..e6a722853 100644 --- a/app/models/form/sales/pages/mortgage_amount.rb +++ b/app/models/form/sales/pages/mortgage_amount.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageAmount < ::Form::Page def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @ownershipsch = ownershipsch - @copy_key = "sales.sale_information.mortgage" @depends_on = [{ "mortgage_used?" => true }] end diff --git a/app/models/form/sales/pages/mortgage_lender.rb b/app/models/form/sales/pages/mortgage_lender.rb index 87646a514..6db3c01df 100644 --- a/app/models/form/sales/pages/mortgage_lender.rb +++ b/app/models/form/sales/pages/mortgage_lender.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageLender < ::Form::Page def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @ownershipsch = ownershipsch - @copy_key = "sales.sale_information.mortgagelender" @description = "" @subsection = subsection @depends_on = [{ diff --git a/app/models/form/sales/pages/mortgage_lender_other.rb b/app/models/form/sales/pages/mortgage_lender_other.rb index 903d6518f..f71089377 100644 --- a/app/models/form/sales/pages/mortgage_lender_other.rb +++ b/app/models/form/sales/pages/mortgage_lender_other.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageLenderOther < ::Form::Page def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @ownershipsch = ownershipsch - @copy_key = "sales.sale_information.mortgagelenderother" @description = "" @subsection = subsection @depends_on = [{ diff --git a/app/models/form/sales/pages/mortgage_length.rb b/app/models/form/sales/pages/mortgage_length.rb index 76c46694a..dbc01a695 100644 --- a/app/models/form/sales/pages/mortgage_length.rb +++ b/app/models/form/sales/pages/mortgage_length.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageLength < ::Form::Page def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @ownershipsch = ownershipsch - @copy_key = "sales.sale_information.mortlen" @depends_on = [{ "mortgageused" => 1, }] diff --git a/app/models/form/sales/pages/mortgageused.rb b/app/models/form/sales/pages/mortgageused.rb index ab48b0c2d..f9d8eae2e 100644 --- a/app/models/form/sales/pages/mortgageused.rb +++ b/app/models/form/sales/pages/mortgageused.rb @@ -1,7 +1,6 @@ class Form::Sales::Pages::Mortgageused < ::Form::Page def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) - @copy_key = "sales.sale_information.mortgageused" @ownershipsch = ownershipsch end diff --git a/app/models/form/sales/pages/previous_bedrooms.rb b/app/models/form/sales/pages/previous_bedrooms.rb index 26b3ef050..214632d49 100644 --- a/app/models/form/sales/pages/previous_bedrooms.rb +++ b/app/models/form/sales/pages/previous_bedrooms.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::PreviousBedrooms < ::Form::Page def initialize(id, hsh, subsection) super @id = "previous_bedrooms" - @copy_key = "sales.sale_information.frombeds" @depends_on = [ { "soctenant" => 1, diff --git a/app/models/form/sales/pages/previous_property_type.rb b/app/models/form/sales/pages/previous_property_type.rb index c5dd4f66a..26669d774 100644 --- a/app/models/form/sales/pages/previous_property_type.rb +++ b/app/models/form/sales/pages/previous_property_type.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::PreviousPropertyType < ::Form::Page def initialize(id, hsh, subsection) super @id = "previous_property_type" - @copy_key = "sales.sale_information.fromprop" @description = "" @subsection = subsection @depends_on = [ diff --git a/app/models/form/sales/pages/previous_tenure.rb b/app/models/form/sales/pages/previous_tenure.rb index c35b6bd67..0f4a4b250 100644 --- a/app/models/form/sales/pages/previous_tenure.rb +++ b/app/models/form/sales/pages/previous_tenure.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::PreviousTenure < ::Form::Page def initialize(id, hsh, subsection) super @id = "shared_ownership_previous_tenure" - @copy_key = "sales.sale_information.socprevten" @header = "" @description = "" @subsection = subsection diff --git a/app/models/form/sales/pages/privacy_notice.rb b/app/models/form/sales/pages/privacy_notice.rb index 40c441d3e..c99ee3397 100644 --- a/app/models/form/sales/pages/privacy_notice.rb +++ b/app/models/form/sales/pages/privacy_notice.rb @@ -1,7 +1,7 @@ class Form::Sales::Pages::PrivacyNotice < ::Form::Page def initialize(id, hsh, subsection, joint_purchase:) super(id, hsh, subsection) - @copy_key = "sales.#{subsection.id}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" + @copy_key = "sales.#{subsection.copy_key}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" @joint_purchase = joint_purchase end diff --git a/app/models/form/sales/pages/resale.rb b/app/models/form/sales/pages/resale.rb index 6f4cd24e2..ffdbbc046 100644 --- a/app/models/form/sales/pages/resale.rb +++ b/app/models/form/sales/pages/resale.rb @@ -2,7 +2,6 @@ class Form::Sales::Pages::Resale < ::Form::Page def initialize(id, hsh, subsection) super @id = "resale" - @copy_key = "sales.sale_information.resale" @depends_on = [ { "staircase" => 2, diff --git a/app/models/form/sales/pages/staircase.rb b/app/models/form/sales/pages/staircase.rb index 1413abadc..c96da3f3f 100644 --- a/app/models/form/sales/pages/staircase.rb +++ b/app/models/form/sales/pages/staircase.rb @@ -3,7 +3,7 @@ class Form::Sales::Pages::Staircase < ::Form::Page super @id = "staircasing" @depends_on = [{ "ownershipsch" => 1 }] - @copy_key = "sales.#{subsection.id}.staircasing" + @copy_key = "sales.#{subsection.copy_key}.staircasing" end def questions 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 c2212c787..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,6 @@ class Form::Sales::Pages::ValueSharedOwnership < ::Form::Page def initialize(id, hsh, subsection) super - @id = "value_shared_ownership" @copy_key = "sales.sale_information.value" end diff --git a/app/models/form/sales/questions/buyer_interview.rb b/app/models/form/sales/questions/buyer_interview.rb index b49b57807..50c290904 100644 --- a/app/models/form/sales/questions/buyer_interview.rb +++ b/app/models/form/sales/questions/buyer_interview.rb @@ -2,7 +2,7 @@ class Form::Sales::Questions::BuyerInterview < ::Form::Question def initialize(id, hsh, page, joint_purchase:) super(id, hsh, page) @id = "noint" - @copy_key = "sales.#{subsection.id}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" + @copy_key = "sales.#{subsection.copy_key}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" @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/deposit_amount.rb b/app/models/form/sales/questions/deposit_amount.rb index 41586cd94..6f2b98ce8 100644 --- a/app/models/form/sales/questions/deposit_amount.rb +++ b/app/models/form/sales/questions/deposit_amount.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::DepositAmount < ::Form::Question def initialize(id, hsh, subsection, ownershipsch:, optional:) super(id, hsh, subsection) @id = "deposit" - @copy_key = "sales.sale_information.deposit" @type = "numeric" @min = 0 @max = 999_999 diff --git a/app/models/form/sales/questions/deposit_discount.rb b/app/models/form/sales/questions/deposit_discount.rb index 289e3962c..bfc5f425d 100644 --- a/app/models/form/sales/questions/deposit_discount.rb +++ b/app/models/form/sales/questions/deposit_discount.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::DepositDiscount < ::Form::Question def initialize(id, hsh, page) super @id = "cashdis" - @copy_key = "sales.sale_information.cashdis" @type = "numeric" @min = 0 @max = 999_999 diff --git a/app/models/form/sales/questions/discount.rb b/app/models/form/sales/questions/discount.rb index 3807a8cfc..5dcf1f125 100644 --- a/app/models/form/sales/questions/discount.rb +++ b/app/models/form/sales/questions/discount.rb @@ -3,7 +3,6 @@ class Form::Sales::Questions::Discount < ::Form::Question super @id = "discount" @type = "numeric" - @copy_key = "sales.sale_information.discount" @min = 0 @max = form.start_year_2024_or_later? ? 70 : 100 @step = 0.1 diff --git a/app/models/form/sales/questions/equity.rb b/app/models/form/sales/questions/equity.rb index 4aae785b8..e39e77ebb 100644 --- a/app/models/form/sales/questions/equity.rb +++ b/app/models/form/sales/questions/equity.rb @@ -2,7 +2,7 @@ class Form::Sales::Questions::Equity < ::Form::Question def initialize(id, hsh, page) super @id = "equity" - @copy_key = "sales.sale_information.equity" + @copy_key = "sales.sale_information.equity.#{page.id}" @type = "numeric" @min = 0 @max = 100 diff --git a/app/models/form/sales/questions/extra_borrowing.rb b/app/models/form/sales/questions/extra_borrowing.rb index e3cd0ff7e..2ad13ef5d 100644 --- a/app/models/form/sales/questions/extra_borrowing.rb +++ b/app/models/form/sales/questions/extra_borrowing.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::ExtraBorrowing < ::Form::Question def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @id = "extrabor" - @copy_key = "sales.sale_information.extrabor" @type = "radio" @answer_options = ANSWER_OPTIONS @page = page diff --git a/app/models/form/sales/questions/fromprop.rb b/app/models/form/sales/questions/fromprop.rb index 1a3393b7a..dec591cd0 100644 --- a/app/models/form/sales/questions/fromprop.rb +++ b/app/models/form/sales/questions/fromprop.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::Fromprop < ::Form::Question def initialize(id, hsh, page) super @id = "fromprop" - @copy_key = "sales.sale_information.fromprop" @type = "radio" @page = page @answer_options = ANSWER_OPTIONS diff --git a/app/models/form/sales/questions/grant.rb b/app/models/form/sales/questions/grant.rb index 17361fe9c..f069fedd2 100644 --- a/app/models/form/sales/questions/grant.rb +++ b/app/models/form/sales/questions/grant.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::Grant < ::Form::Question def initialize(id, hsh, page) super @id = "grant" - @copy_key = "sales.sale_information.grant" @type = "numeric" @min = 0 @max = 999_999 diff --git a/app/models/form/sales/questions/has_management_fee.rb b/app/models/form/sales/questions/has_management_fee.rb new file mode 100644 index 000000000..20a71ff5e --- /dev/null +++ b/app/models/form/sales/questions/has_management_fee.rb @@ -0,0 +1,24 @@ +class Form::Sales::Questions::HasManagementFee < ::Form::Question + def initialize(id, hsh, subsection) + super + @id = "has_management_fee" + @copy_key = "sales.sale_information.management_fee.has_management_fee" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @conditional_for = { + "management_fee" => [1], + } + @hidden_in_check_answers = { + "depends_on" => [ + { + "has_management_fee" => 1, + }, + ], + } + end + + ANSWER_OPTIONS = { + "1" => { "value" => "Yes" }, + "0" => { "value" => "No" }, + }.freeze +end diff --git a/app/models/form/sales/questions/management_fee.rb b/app/models/form/sales/questions/management_fee.rb new file mode 100644 index 000000000..213b9e3df --- /dev/null +++ b/app/models/form/sales/questions/management_fee.rb @@ -0,0 +1,12 @@ +class Form::Sales::Questions::ManagementFee < ::Form::Question + def initialize(id, hsh, subsection) + super + @id = "management_fee" + @copy_key = "sales.sale_information.management_fee.management_fee" + @type = "numeric" + @min = 1 + @step = 0.01 + @width = 5 + @prefix = "£" + end +end diff --git a/app/models/form/sales/questions/monthly_rent.rb b/app/models/form/sales/questions/monthly_rent.rb index 7e64d8571..8e9ecfaef 100644 --- a/app/models/form/sales/questions/monthly_rent.rb +++ b/app/models/form/sales/questions/monthly_rent.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::MonthlyRent < ::Form::Question def initialize(id, hsh, page) super @id = "mrent" - @copy_key = "sales.sale_information.mrent" @type = "numeric" @min = 0 @step = 0.01 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/mortgage_amount.rb b/app/models/form/sales/questions/mortgage_amount.rb index a6ffcf26a..e0677ee18 100644 --- a/app/models/form/sales/questions/mortgage_amount.rb +++ b/app/models/form/sales/questions/mortgage_amount.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageAmount < ::Form::Question def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @id = "mortgage" - @copy_key = "sales.sale_information.mortgage" @type = "numeric" @min = 1 @step = 1 diff --git a/app/models/form/sales/questions/mortgage_lender.rb b/app/models/form/sales/questions/mortgage_lender.rb index a4aa55f17..96bf9e5b3 100644 --- a/app/models/form/sales/questions/mortgage_lender.rb +++ b/app/models/form/sales/questions/mortgage_lender.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageLender < ::Form::Question def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @id = "mortgagelender" - @copy_key = "sales.sale_information.mortgagelender" @type = "select" @page = page @bottom_guidance_partial = "mortgage_lender" diff --git a/app/models/form/sales/questions/mortgage_lender_other.rb b/app/models/form/sales/questions/mortgage_lender_other.rb index 49876efb0..8cd5de8fb 100644 --- a/app/models/form/sales/questions/mortgage_lender_other.rb +++ b/app/models/form/sales/questions/mortgage_lender_other.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageLenderOther < ::Form::Question def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @id = "mortgagelenderother" - @copy_key = "sales.sale_information.mortgagelenderother" @type = "text" @page = page @ownershipsch = ownershipsch diff --git a/app/models/form/sales/questions/mortgage_length.rb b/app/models/form/sales/questions/mortgage_length.rb index 877818b98..5d94fc832 100644 --- a/app/models/form/sales/questions/mortgage_length.rb +++ b/app/models/form/sales/questions/mortgage_length.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageLength < ::Form::Question def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @id = "mortlen" - @copy_key = "sales.sale_information.mortlen" @type = "numeric" @min = 0 @max = 60 diff --git a/app/models/form/sales/questions/mortgageused.rb b/app/models/form/sales/questions/mortgageused.rb index e4a101072..1c683384b 100644 --- a/app/models/form/sales/questions/mortgageused.rb +++ b/app/models/form/sales/questions/mortgageused.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::Mortgageused < ::Form::Question def initialize(id, hsh, subsection, ownershipsch:) super(id, hsh, subsection) @id = "mortgageused" - @copy_key = "sales.sale_information.mortgageused" @type = "radio" @answer_options = ANSWER_OPTIONS @ownershipsch = ownershipsch @@ -13,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/previous_bedrooms.rb b/app/models/form/sales/questions/previous_bedrooms.rb index d29da208a..dd243137d 100644 --- a/app/models/form/sales/questions/previous_bedrooms.rb +++ b/app/models/form/sales/questions/previous_bedrooms.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::PreviousBedrooms < ::Form::Question def initialize(id, hsh, page) super @id = "frombeds" - @copy_key = "sales.sale_information.frombeds" @type = "numeric" @width = 5 @min = 1 diff --git a/app/models/form/sales/questions/previous_tenure.rb b/app/models/form/sales/questions/previous_tenure.rb index 55b103f0d..794d449b5 100644 --- a/app/models/form/sales/questions/previous_tenure.rb +++ b/app/models/form/sales/questions/previous_tenure.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::PreviousTenure < ::Form::Question def initialize(id, hsh, page) super @id = "socprevten" - @copy_key = "sales.sale_information.socprevten" @type = "radio" @page = page @answer_options = ANSWER_OPTIONS diff --git a/app/models/form/sales/questions/privacy_notice.rb b/app/models/form/sales/questions/privacy_notice.rb index 5e73e7a3a..ace2c9ec1 100644 --- a/app/models/form/sales/questions/privacy_notice.rb +++ b/app/models/form/sales/questions/privacy_notice.rb @@ -2,7 +2,7 @@ class Form::Sales::Questions::PrivacyNotice < ::Form::Question def initialize(id, hsh, page, joint_purchase:) super(id, hsh, page) @id = "privacynotice" - @copy_key = "sales.#{subsection.id}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" + @copy_key = "sales.#{subsection.copy_key}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}" @type = "checkbox" @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] @joint_purchase = joint_purchase diff --git a/app/models/form/sales/questions/resale.rb b/app/models/form/sales/questions/resale.rb index 0026adb48..2417960b4 100644 --- a/app/models/form/sales/questions/resale.rb +++ b/app/models/form/sales/questions/resale.rb @@ -2,7 +2,6 @@ class Form::Sales::Questions::Resale < ::Form::Question def initialize(id, hsh, page) super @id = "resale" - @copy_key = "sales.sale_information.resale" @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/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 257344fa2..ad021e920 100644 --- a/app/models/form/sales/questions/value.rb +++ b/app/models/form/sales/questions/value.rb @@ -2,7 +2,7 @@ class Form::Sales::Questions::Value < ::Form::Question def initialize(id, hsh, page) super @id = "value" - @copy_key = "sales.sale_information.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 b57eb70a6..fc2180529 100644 --- a/app/models/form/sales/sections/sale_information.rb +++ b/app/models/form/sales/sections/sale_information.rb @@ -4,10 +4,20 @@ class Form::Sales::Sections::SaleInformation < ::Form::Section @id = "sale_information" @label = "Sale information" @description = "" - @subsections = [ - Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self), - 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::SharedOwnershipStaircasingTransaction.new(nil, nil, self), + ] + else + [Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self)] + end end end diff --git a/app/models/form/sales/subsections/discounted_ownership_scheme.rb b/app/models/form/sales/subsections/discounted_ownership_scheme.rb index 63ce4af47..c74dcd262 100644 --- a/app/models/form/sales/subsections/discounted_ownership_scheme.rb +++ b/app/models/form/sales/subsections/discounted_ownership_scheme.rb @@ -4,6 +4,7 @@ class Form::Sales::Subsections::DiscountedOwnershipScheme < ::Form::Subsection @id = "discounted_ownership_scheme" @label = "Discounted ownership scheme" @depends_on = [{ "ownershipsch" => 2, "setup_completed?" => true }] + @copy_key = "sale_information" end def pages diff --git a/app/models/form/sales/subsections/household_characteristics.rb b/app/models/form/sales/subsections/household_characteristics.rb index e839b1979..5c3c0b9e3 100644 --- a/app/models/form/sales/subsections/household_characteristics.rb +++ b/app/models/form/sales/subsections/household_characteristics.rb @@ -35,7 +35,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Sales::Pages::Buyer1WorkingSituation.new(nil, nil, self), Form::Sales::Pages::RetirementValueCheck.new("working_situation_1_retirement_value_check", nil, self, person_index: 1), (Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_1_not_retired_value_check", nil, self, person_index: 1) if form.start_year_2024_or_later?), - Form::Sales::Pages::Buyer1IncomeMinValueCheck.new("working_situation_buyer_1_income_min_value_check", nil, self), + Form::Sales::Pages::Buyer1IncomeEcstatValueCheck.new("working_situation_buyer_1_income_value_check", nil, self), Form::Sales::Pages::Buyer1LiveInProperty.new(nil, nil, self), Form::Sales::Pages::BuyerLiveInValueCheck.new("buyer_1_live_in_property_value_check", nil, self, person_index: 1), (form.start_year_2025_or_later? ? Form::Sales::Pages::Buyer2RelationshipToBuyer1YesNo.new(nil, nil, self) : Form::Sales::Pages::Buyer2RelationshipToBuyer1.new(nil, nil, self)), @@ -51,7 +51,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self), Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_retirement_value_check_joint_purchase", nil, self, person_index: 2), (Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_2_not_retired_value_check_joint_purchase", nil, self, person_index: 2) if form.start_year_2024_or_later?), - Form::Sales::Pages::Buyer2IncomeMinValueCheck.new("working_situation_buyer_2_income_min_value_check", nil, self), + Form::Sales::Pages::Buyer2IncomeEcstatValueCheck.new("working_situation_buyer_2_income_value_check", nil, self), Form::Sales::Pages::PersonStudentNotChildValueCheck.new("buyer_2_working_situation_student_not_child_value_check", nil, self, person_index: 2), Form::Sales::Pages::Buyer2LiveInProperty.new(nil, nil, self), Form::Sales::Pages::BuyerLiveInValueCheck.new("buyer_2_live_in_property_value_check", nil, self, person_index: 2), diff --git a/app/models/form/sales/subsections/income_benefits_and_savings.rb b/app/models/form/sales/subsections/income_benefits_and_savings.rb index 9244267b9..84d3c49a9 100644 --- a/app/models/form/sales/subsections/income_benefits_and_savings.rb +++ b/app/models/form/sales/subsections/income_benefits_and_savings.rb @@ -16,16 +16,16 @@ class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection def pages @pages ||= [ Form::Sales::Pages::Buyer1Income.new(nil, nil, self), - Form::Sales::Pages::Buyer1IncomeMinValueCheck.new("buyer_1_income_min_value_check", nil, self), - Form::Sales::Pages::Buyer1IncomeMaxValueCheck.new("buyer_1_income_max_value_check", nil, self, check_answers_card_number: 1), + Form::Sales::Pages::Buyer1IncomeEcstatValueCheck.new("buyer_1_income_ecstat_value_check", nil, self), + Form::Sales::Pages::Buyer1IncomeDiscountedMaxValueCheck.new("buyer_1_income_discounted_max_value_check", nil, self, check_answers_card_number: 1), Form::Sales::Pages::CombinedIncomeMaxValueCheck.new("buyer_1_combined_income_max_value_check", nil, self, check_answers_card_number: 1), Form::Sales::Pages::MortgageValueCheck.new("buyer_1_income_mortgage_value_check", nil, self, 1), Form::Sales::Pages::Buyer1Mortgage.new(nil, nil, self), Form::Sales::Pages::MortgageValueCheck.new("buyer_1_mortgage_value_check", nil, self, 1), Form::Sales::Pages::Buyer2Income.new(nil, nil, self), Form::Sales::Pages::MortgageValueCheck.new("buyer_2_income_mortgage_value_check", nil, self, 2), - Form::Sales::Pages::Buyer2IncomeMinValueCheck.new("buyer_2_income_min_value_check", nil, self), - Form::Sales::Pages::Buyer2IncomeMaxValueCheck.new("buyer_2_income_max_value_check", nil, self, check_answers_card_number: 2), + Form::Sales::Pages::Buyer2IncomeEcstatValueCheck.new("buyer_2_income_ecstat_value_check", nil, self), + Form::Sales::Pages::Buyer2IncomeDiscountedMaxValueCheck.new("buyer_2_income_discounted_max_value_check", nil, self, check_answers_card_number: 2), Form::Sales::Pages::CombinedIncomeMaxValueCheck.new("buyer_2_combined_income_max_value_check", nil, self, check_answers_card_number: 2), Form::Sales::Pages::Buyer2Mortgage.new(nil, nil, self), Form::Sales::Pages::MortgageValueCheck.new("buyer_2_mortgage_value_check", nil, self, 2), diff --git a/app/models/form/sales/subsections/outright_sale.rb b/app/models/form/sales/subsections/outright_sale.rb index af63c8179..afa0f4a69 100644 --- a/app/models/form/sales/subsections/outright_sale.rb +++ b/app/models/form/sales/subsections/outright_sale.rb @@ -4,6 +4,7 @@ class Form::Sales::Subsections::OutrightSale < ::Form::Subsection @id = "outright_sale" @label = "Outright sale" @depends_on = [{ "ownershipsch" => 3, "setup_completed?" => true }] + @copy_key = "sale_information" end def pages diff --git a/app/models/form/sales/subsections/property_information.rb b/app/models/form/sales/subsections/property_information.rb index 5d4021681..28c0ad004 100644 --- a/app/models/form/sales/subsections/property_information.rb +++ b/app/models/form/sales/subsections/property_information.rb @@ -31,8 +31,8 @@ class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection Form::Sales::Pages::UprnSelection.new(nil, nil, self), Form::Sales::Pages::AddressFallback.new(nil, nil, self), Form::Sales::Pages::PropertyLocalAuthority.new(nil, nil, self), - Form::Sales::Pages::Buyer1IncomeMaxValueCheck.new("local_authority_buyer_1_income_max_value_check", nil, self, check_answers_card_number: nil), - Form::Sales::Pages::Buyer2IncomeMaxValueCheck.new("local_authority_buyer_2_income_max_value_check", nil, self, check_answers_card_number: nil), + Form::Sales::Pages::Buyer1IncomeDiscountedMaxValueCheck.new("local_authority_buyer_1_income_max_value_check", nil, self, check_answers_card_number: nil), + Form::Sales::Pages::Buyer2IncomeDiscountedMaxValueCheck.new("local_authority_buyer_2_income_max_value_check", nil, self, check_answers_card_number: nil), Form::Sales::Pages::CombinedIncomeMaxValueCheck.new("local_authority_combined_income_max_value_check", nil, self, check_answers_card_number: nil), Form::Sales::Pages::AboutPriceValueCheck.new("about_price_la_value_check", nil, self), ] @@ -42,8 +42,8 @@ class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection Form::Sales::Pages::UprnConfirmation.new(nil, nil, self), Form::Sales::Pages::Address.new(nil, nil, self), Form::Sales::Pages::PropertyLocalAuthority.new(nil, nil, self), - Form::Sales::Pages::Buyer1IncomeMaxValueCheck.new("local_authority_buyer_1_income_max_value_check", nil, self, check_answers_card_number: nil), - Form::Sales::Pages::Buyer2IncomeMaxValueCheck.new("local_authority_buyer_2_income_max_value_check", nil, self, check_answers_card_number: nil), + Form::Sales::Pages::Buyer1IncomeDiscountedMaxValueCheck.new("local_authority_buyer_1_income_max_value_check", nil, self, check_answers_card_number: nil), + Form::Sales::Pages::Buyer2IncomeDiscountedMaxValueCheck.new("local_authority_buyer_2_income_max_value_check", nil, self, check_answers_card_number: nil), Form::Sales::Pages::CombinedIncomeMaxValueCheck.new("local_authority_combined_income_max_value_check", nil, self, check_answers_card_number: nil), Form::Sales::Pages::AboutPriceValueCheck.new("about_price_la_value_check", nil, self), ] diff --git a/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb b/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb new file mode 100644 index 000000000..175994b0b --- /dev/null +++ b/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb @@ -0,0 +1,49 @@ +class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsection + def initialize(id, hsh, section) + super + @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 + @pages ||= [ + Form::Sales::Pages::Resale.new(nil, nil, self), + Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership_joint_purchase", nil, self, ownershipsch: 1, joint_purchase: true), + Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self, ownershipsch: 1, joint_purchase: false), + Form::Sales::Pages::HandoverDate.new(nil, nil, self), + Form::Sales::Pages::HandoverDateCheck.new(nil, nil, self), + Form::Sales::Pages::BuyerPrevious.new("buyer_previous_joint_purchase", nil, self, joint_purchase: true), + Form::Sales::Pages::BuyerPrevious.new("buyer_previous_not_joint_purchase", nil, self, joint_purchase: false), + 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("value_shared_ownership", nil, self), + Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", 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), + Form::Sales::Pages::MortgageAmount.new("mortgage_amount_shared_ownership", nil, self, ownershipsch: 1), + Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_mortgage_amount_value_check", nil, self), + Form::Sales::Pages::MortgageValueCheck.new("mortgage_amount_mortgage_value_check", nil, self), + Form::Sales::Pages::MortgageLength.new("mortgage_length_shared_ownership", nil, self, ownershipsch: 1), + Form::Sales::Pages::Deposit.new("deposit_shared_ownership", nil, self, ownershipsch: 1, optional: false), + Form::Sales::Pages::Deposit.new("deposit_shared_ownership_optional", nil, self, ownershipsch: 1, optional: true), + Form::Sales::Pages::DepositValueCheck.new("deposit_joint_purchase_value_check", nil, self, joint_purchase: true), + Form::Sales::Pages::DepositValueCheck.new("deposit_value_check", nil, self, joint_purchase: false), + Form::Sales::Pages::DepositDiscount.new("deposit_discount", nil, self, optional: false), + Form::Sales::Pages::DepositDiscount.new("deposit_discount_optional", nil, self, optional: true), + Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_deposit_value_check", nil, self), + Form::Sales::Pages::MonthlyRent.new(nil, nil, self), + Form::Sales::Pages::LeaseholdCharges.new("leasehold_charges_shared_ownership", nil, self, ownershipsch: 1), + Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self), + Form::Sales::Pages::EstateManagementFee.new("estate_management_fee", nil, self), + ].compact + end + + def displayed_in_tasklist?(log) + log.staircase == 2 && (log.ownershipsch.nil? || log.ownershipsch == 1) + end +end diff --git a/app/models/form/sales/subsections/shared_ownership_scheme.rb b/app/models/form/sales/subsections/shared_ownership_scheme.rb index f5d52153e..c0718e009 100644 --- a/app/models/form/sales/subsections/shared_ownership_scheme.rb +++ b/app/models/form/sales/subsections/shared_ownership_scheme.rb @@ -11,7 +11,7 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection @pages ||= [ Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership_joint_purchase", nil, self, ownershipsch: 1, joint_purchase: true), Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self, ownershipsch: 1, joint_purchase: false), - (Form::Sales::Pages::Staircase.new(nil, nil, self) unless form.start_year_2025_or_later?), + Form::Sales::Pages::Staircase.new(nil, nil, self), 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::StaircaseBoughtValueCheck.new(nil, nil, self), @@ -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 03af24a94..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") } @@ -54,13 +55,13 @@ class Location < ApplicationRecord } scope :deactivated, lambda { |date = Time.zone.now| - deactivated_by_organisation + deactivated_by_organisation(date) .or(deactivated_directly(date)) .or(deactivated_by_scheme(date)) } - scope :deactivated_by_organisation, lambda { - merge(Organisation.filter_by_inactive) + scope :deactivated_by_organisation, lambda { |date = Time.zone.now| + merge(Organisation.filter_by_inactive.or(Organisation.where("merge_date <= ?", date))) } scope :deactivated_by_scheme, lambda { |date = Time.zone.now| @@ -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 @@ -206,7 +230,7 @@ class Location < ApplicationRecord def status_at(date) return :deleted if discarded_at.present? return :incomplete unless confirmed - return :deactivated if scheme.owning_organisation.status_at(date) == :deactivated || + return :deactivated if scheme.owning_organisation.status_at(date) == :deactivated || scheme.owning_organisation.status_at(date) == :merged || open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date || scheme.status_at(date) == :deactivated return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date || scheme.status_at(date) == :deactivating_soon return :activating_soon if startdate.present? && date < startdate diff --git a/app/models/log.rb b/app/models/log.rb index aed375117..dd4301550 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -111,6 +111,7 @@ class Log < ApplicationRecord self.town_or_city = nil self.county = nil self.postcode_full = postcode_full_input + process_postcode_changes! else self.uprn = uprn_selection self.uprn_confirmed = 1 diff --git a/app/models/merge_request_organisation.rb b/app/models/merge_request_organisation.rb index 6dda8b35e..5bfbe14d7 100644 --- a/app/models/merge_request_organisation.rb +++ b/app/models/merge_request_organisation.rb @@ -29,5 +29,12 @@ private if merging_organisation_id.blank? || !Organisation.where(id: merging_organisation_id).exists? merge_request.errors.add(:merging_organisation, I18n.t("validations.merge_request.organisation_not_selected")) end + + existing_merges = MergeRequestOrganisation.with_merging_organisation(merging_organisation) + if existing_merges.count.positive? + existing_merge_request = existing_merges.first.merge_request + errors.add(:merging_organisation, I18n.t("validations.merge_request.organisation_part_of_another_merge")) + merge_request.errors.add(:merging_organisation, I18n.t("validations.merge_request.organisation_part_of_another_incomplete_merge", organisation: merging_organisation.name, absorbing_organisation: existing_merge_request.absorbing_organisation&.name, merge_date: existing_merge_request.merge_date&.to_fs(:govuk_date))) + end end end 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 2c73acc06..1cd56ac7d 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -57,8 +57,8 @@ class Scheme < ApplicationRecord .or(deactivated_directly) } - scope :deactivated_by_organisation, lambda { - merge(Organisation.filter_by_inactive) + scope :deactivated_by_organisation, lambda { |date = Time.zone.now| + merge(Organisation.filter_by_inactive.or(Organisation.where("merge_date <= ?", date))) } scope :deactivated_directly, lambda { |date = Time.zone.now| @@ -96,7 +96,7 @@ class Scheme < ApplicationRecord scope :active, lambda { |date = Time.zone.now| where.not(id: joins(:scheme_deactivation_periods).reactivating_soon(date).pluck(:id)) .where.not(id: incomplete.pluck(:id)) - .where.not(id: joins(:owning_organisation).deactivated_by_organisation.pluck(:id)) + .where.not(id: joins(:owning_organisation).deactivated_by_organisation(date).pluck(:id)) .where.not(id: joins(:owning_organisation).joins(:scheme_deactivation_periods).deactivated_directly(date).pluck(:id)) .where.not(id: activating_soon(date).pluck(:id)) } @@ -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 @@ -314,7 +330,7 @@ class Scheme < ApplicationRecord def status_at(date) return :deleted if discarded_at.present? return :incomplete unless confirmed && locations.confirmed.any? - return :deactivated if owning_organisation.status_at(date) == :deactivated || + return :deactivated if owning_organisation.status_at(date) == :deactivated || owning_organisation.status_at(date) == :merged || (open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date) return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date 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/models/validations/sales/soft_validations.rb b/app/models/validations/sales/soft_validations.rb index d53391dd1..2bc574774 100644 --- a/app/models/validations/sales/soft_validations.rb +++ b/app/models/validations/sales/soft_validations.rb @@ -2,41 +2,59 @@ module Validations::Sales::SoftValidations include Validations::Sales::SaleInformationValidations ALLOWED_INCOME_RANGES_SALES = { - 1 => OpenStruct.new(soft_min: 5000), - 2 => OpenStruct.new(soft_min: 1500), - 3 => OpenStruct.new(soft_min: 1000), - 5 => OpenStruct.new(soft_min: 2000), - 0 => OpenStruct.new(soft_min: 2000), + 2024 => { + 1 => OpenStruct.new(soft_min: 5000), + 2 => OpenStruct.new(soft_min: 1500), + 3 => OpenStruct.new(soft_min: 1000), + 5 => OpenStruct.new(soft_min: 2000), + 0 => OpenStruct.new(soft_min: 2000), + }, + 2025 => { + 1 => OpenStruct.new(soft_min: 13_400, soft_max: 150_000), + 2 => OpenStruct.new(soft_min: 2_600, soft_max: 80_000), + 3 => OpenStruct.new(soft_min: 2_080, soft_max: 30_000), + 4 => OpenStruct.new(soft_min: 520, soft_max: 23_400), + 5 => OpenStruct.new(soft_min: 520, soft_max: 80_000), + 6 => OpenStruct.new(soft_min: 520, soft_max: 50_000), + 7 => OpenStruct.new(soft_min: 520, soft_max: 30_000), + 8 => OpenStruct.new(soft_min: 520, soft_max: 150_000), + 9 => OpenStruct.new(soft_min: 520, soft_max: 150_000), + 0 => OpenStruct.new(soft_min: 520, soft_max: 150_000), + }, }.freeze - def income1_under_soft_min? - return false unless ecstat1 && income1 && ALLOWED_INCOME_RANGES_SALES[ecstat1] + def income1_outside_soft_range_for_ecstat? + income1_under_soft_min? || income1_over_soft_max_for_ecstat? + end - income1 < ALLOWED_INCOME_RANGES_SALES[ecstat1][:soft_min] + def income1_more_or_less_text + income1_under_soft_min? ? "less" : "more" end - def income2_under_soft_min? - return false unless ecstat2 && income2 && ALLOWED_INCOME_RANGES_SALES[ecstat2] + def income2_outside_soft_range_for_ecstat? + income2_under_soft_min? || income2_over_soft_max_for_ecstat? + end - income2 < ALLOWED_INCOME_RANGES_SALES[ecstat2][:soft_min] + def income2_more_or_less_text + income2_under_soft_min? ? "less" : "more" end - def income1_over_soft_max? + def income1_over_soft_max_for_discounted_ownership? return unless income1 && la && discounted_ownership_sale? - income_over_soft_max?(income1) + income_over_discounted_sale_soft_max?(income1) end - def income2_over_soft_max? + def income2_over_soft_max_for_discounted_ownership? return unless income2 && la && discounted_ownership_sale? - income_over_soft_max?(income2) + income_over_discounted_sale_soft_max?(income2) end - def combined_income_over_soft_max? + def combined_income_over_soft_max_for_discounted_ownership? return unless income1 && income2 && la && discounted_ownership_sale? - income_over_soft_max?(income1 + income2) + income_over_discounted_sale_soft_max?(income1 + income2) end def staircase_bought_above_fifty? @@ -192,7 +210,40 @@ private ) end - def income_over_soft_max?(income) + def income1_under_soft_min? + income_under_soft_min?(income1, ecstat1) + end + + def income2_under_soft_min? + income_under_soft_min?(income2, ecstat2) + end + + def income_under_soft_min?(income, ecstat) + return unless income && ecstat + + income_ranges = form.start_year_2025_or_later? ? ALLOWED_INCOME_RANGES_SALES[2025] : ALLOWED_INCOME_RANGES_SALES[2024] + return false unless income_ranges[ecstat] + + income < income_ranges[ecstat][:soft_min] + end + + def income1_over_soft_max_for_ecstat? + income_over_soft_max?(income1, ecstat1) + end + + def income2_over_soft_max_for_ecstat? + income_over_soft_max?(income2, ecstat2) + end + + def income_over_soft_max?(income, ecstat) + return unless income && ecstat && form.start_year_2025_or_later? + + return false unless ALLOWED_INCOME_RANGES_SALES[2025][ecstat] + + income > ALLOWED_INCOME_RANGES_SALES[2025][ecstat][:soft_max] + end + + def income_over_discounted_sale_soft_max?(income) (london_property? && income > 90_000) || (property_not_in_london? && income > 80_000) end end diff --git a/app/policies/csv_download_policy.rb b/app/policies/csv_download_policy.rb new file mode 100644 index 000000000..04471ccd0 --- /dev/null +++ b/app/policies/csv_download_policy.rb @@ -0,0 +1,16 @@ +class CsvDownloadPolicy + attr_reader :current_user, :csv_download + + def initialize(current_user, csv_download) + @current_user = current_user + @csv_download = csv_download + end + + def show? + @current_user == @csv_download.user || @current_user.support? || @current_user.organisation == @csv_download.organisation + end + + def download? + @current_user == @csv_download.user || @current_user.support? || @current_user.organisation == @csv_download.organisation + end +end diff --git a/app/policies/location_policy.rb b/app/policies/location_policy.rb index 436b961c6..3b4a22131 100644 --- a/app/policies/location_policy.rb +++ b/app/policies/location_policy.rb @@ -16,14 +16,14 @@ class LocationPolicy if location == Location user.data_coordinator? else - user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner + user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end end def update? return true if user.support? - user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner + user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end def delete_confirmation? @@ -62,7 +62,7 @@ class LocationPolicy define_method method_name do return true if user.support? - user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner + user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end end @@ -73,7 +73,7 @@ class LocationPolicy define_method method_name do return true if user.support? - scheme_owned_by_user_org_or_stock_owner + scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end end @@ -83,8 +83,11 @@ private location.scheme end - def scheme_owned_by_user_org_or_stock_owner - scheme&.owning_organisation == user.organisation || user.organisation.stock_owners.exists?(scheme&.owning_organisation_id) + def scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org + scheme_owned_by_user_org = scheme&.owning_organisation == user.organisation + scheme_owned_by_stock_owner = user.organisation.stock_owners.exists?(scheme&.owning_organisation_id) + scheme_owned_by_recently_absorbed_org = user.organisation.absorbed_organisations.visible.merged_during_open_collection_period.exists?(scheme&.owning_organisation_id) + scheme_owned_by_user_org || scheme_owned_by_stock_owner || scheme_owned_by_recently_absorbed_org end def has_any_logs_in_editable_collection_period diff --git a/app/policies/scheme_policy.rb b/app/policies/scheme_policy.rb index 6b97a46de..54a2b9e89 100644 --- a/app/policies/scheme_policy.rb +++ b/app/policies/scheme_policy.rb @@ -12,7 +12,7 @@ class SchemePolicy if scheme == Scheme true else - scheme_owned_by_user_org_or_stock_owner + scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end end @@ -27,7 +27,7 @@ class SchemePolicy def update? return true if user.support? - user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner + user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end def changes? @@ -41,7 +41,7 @@ class SchemePolicy define_method method_name do return true if user.support? - scheme_owned_by_user_org_or_stock_owner + scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end end @@ -61,7 +61,7 @@ class SchemePolicy define_method method_name do return true if user.support? - user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner + user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org end end @@ -78,8 +78,11 @@ class SchemePolicy private - def scheme_owned_by_user_org_or_stock_owner - scheme&.owning_organisation == user.organisation || user.organisation.stock_owners.exists?(scheme&.owning_organisation_id) + def scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org + scheme_owned_by_user_org = scheme&.owning_organisation == user.organisation + scheme_owned_by_stock_owner = user.organisation.stock_owners.exists?(scheme&.owning_organisation_id) + scheme_owned_by_recently_absorbed_org = user.organisation.absorbed_organisations.visible.merged_during_open_collection_period.exists?(scheme&.owning_organisation_id) + scheme_owned_by_user_org || scheme_owned_by_stock_owner || scheme_owned_by_recently_absorbed_org end def has_any_logs_in_editable_collection_period diff --git a/app/services/bulk_upload/lettings/year2024/csv_parser.rb b/app/services/bulk_upload/lettings/year2024/csv_parser.rb index 22caeab02..08e12353b 100644 --- a/app/services/bulk_upload/lettings/year2024/csv_parser.rb +++ b/app/services/bulk_upload/lettings/year2024/csv_parser.rb @@ -15,7 +15,7 @@ class BulkUpload::Lettings::Year2024::CsvParser def row_offset if with_headers? - rows.find_index { |row| row[0].match(/field number/i) } + 1 + rows.find_index { |row| row[0].present? && row[0].match(/field number/i) } + 1 else 0 end diff --git a/app/services/bulk_upload/sales/year2024/csv_parser.rb b/app/services/bulk_upload/sales/year2024/csv_parser.rb index 4a3cb7ac9..b20c5b3d3 100644 --- a/app/services/bulk_upload/sales/year2024/csv_parser.rb +++ b/app/services/bulk_upload/sales/year2024/csv_parser.rb @@ -15,7 +15,7 @@ class BulkUpload::Sales::Year2024::CsvParser def row_offset if with_headers? - rows.find_index { |row| row[0].match(/field number/i) } + 1 + rows.find_index { |row| row[0].present? && row[0].match(/field number/i) } + 1 else 0 end diff --git a/app/services/csv/downloader.rb b/app/services/csv/downloader.rb new file mode 100644 index 000000000..24545bc41 --- /dev/null +++ b/app/services/csv/downloader.rb @@ -0,0 +1,50 @@ +class Csv::Downloader + attr_reader :csv_download + + delegate :path, to: :file + + def initialize(csv_download:) + @csv_download = csv_download + end + + def call + download + end + + def delete_local_file! + file.unlink + end + + def presigned_url + s3_storage_service.get_presigned_url(csv_download.filename, 60, response_content_disposition: "attachment; filename=#{csv_download.filename}") + end + +private + + def download + io = storage_service.get_file_io(csv_download.filename) + file.write(io.read) + io.close + file.close + end + + def file + @file ||= Tempfile.new + end + + def storage_service + @storage_service ||= if FeatureToggle.upload_enabled? + s3_storage_service + else + local_disk_storage_service + end + end + + def s3_storage_service + Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"]) + end + + def local_disk_storage_service + Storage::LocalDiskService.new + end +end diff --git a/app/services/feature_toggle.rb b/app/services/feature_toggle.rb index 1b67b8b37..065c3b54e 100644 --- a/app/services/feature_toggle.rb +++ b/app/services/feature_toggle.rb @@ -46,4 +46,8 @@ class FeatureToggle def self.managing_resources_enabled? !Rails.env.production? end + + def self.create_test_logs_enabled? + Rails.env.development? || Rails.env.review? + end end diff --git a/app/services/mandatory_collection_resources_service.rb b/app/services/mandatory_collection_resources_service.rb index 397e4b5d0..197e521d4 100644 --- a/app/services/mandatory_collection_resources_service.rb +++ b/app/services/mandatory_collection_resources_service.rb @@ -46,7 +46,7 @@ class MandatoryCollectionResourcesService year_range = "#{year} to #{year + 1}" case resource when "paper_form" - "#{log_type} log for tenants (#{year_range})" + "#{log_type} paper form (#{year_range})" when "bulk_upload_template" "#{log_type} bulk upload template (#{year_range})" when "bulk_upload_specification" diff --git a/app/views/csv_downloads/show.html.erb b/app/views/csv_downloads/show.html.erb new file mode 100644 index 000000000..18f8a67fe --- /dev/null +++ b/app/views/csv_downloads/show.html.erb @@ -0,0 +1,10 @@ +<% title = "Downlaod CSV file" %> +<% content_for :title, title %> + +
+
+

You are about to download a CSV file

+

Filename: <%= @csv_download.filename %>

+ <%= govuk_button_link_to "Download CSV", download_csv_download_path(@csv_download) %> +
+
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/errors/download_link_expired.html.erb b/app/views/errors/download_link_expired.html.erb new file mode 100644 index 000000000..7477ee6b3 --- /dev/null +++ b/app/views/errors/download_link_expired.html.erb @@ -0,0 +1,8 @@ +<% content_for :title, "This link has expired" %> + +
+
+

This link has expired.

+

Download the logs again to get a new link.

+
+
diff --git a/app/views/form/guidance/_financial_calculations_shared_ownership.html.erb b/app/views/form/guidance/_financial_calculations_shared_ownership.html.erb index 0741e6afa..2dd2f343e 100644 --- a/app/views/form/guidance/_financial_calculations_shared_ownership.html.erb +++ b/app/views/form/guidance/_financial_calculations_shared_ownership.html.erb @@ -20,11 +20,11 @@ <% end %> must equal the purchase price <%= question_link("value", log, current_user) %> - <% stairbought_page = log.form.get_question("stairbought", log).page %> - <% if stairbought_page.routed_to?(log, current_user) %> + <% stairbought_page = log.form.get_question("stairbought", log)&.page %> + <% if stairbought_page&.routed_to?(log, current_user) %> multiplied by the percentage bought <%= question_link("stairbought", log, current_user) %> <% else %> - multiplied by the percentage equity stake <%= question_link("equity", log, current_user) %> + multiplied by the percentage equity share <%= question_link("equity", log, current_user) %> <% end %>

<% end %> diff --git a/app/views/locations/index.html.erb b/app/views/locations/index.html.erb index 64d9bf286..23550f894 100644 --- a/app/views/locations/index.html.erb +++ b/app/views/locations/index.html.erb @@ -56,14 +56,13 @@ <% end %> <% end %> - <% if status_hint_message = scheme_status_hint(@scheme) %> -
- <%= status_hint_message %> -
-
- <% end %> - - <% if LocationPolicy.new(current_user, @scheme.locations.new).create? %> + <% if LocationPolicy.new(current_user, @scheme.locations.new).create? && [:active, :merged].include?(@scheme.owning_organisation.status) %> + <% if status_hint_message = scheme_status_hint(@scheme) %> +
+ <%= status_hint_message %> +
+
+ <% end %> <%= govuk_button_to "Add a location", scheme_locations_path(@scheme), method: "post" %> <% end %>
diff --git a/app/views/locations/show.html.erb b/app/views/locations/show.html.erb index 8ac8f6b23..f9ba6496c 100644 --- a/app/views/locations/show.html.erb +++ b/app/views/locations/show.html.erb @@ -47,7 +47,7 @@ -<% if @location.scheme.owning_organisation.active? && LocationPolicy.new(current_user, @location).deactivate? %> +<% if @location.scheme.owning_organisation.status == :active && LocationPolicy.new(current_user, @location).deactivate? %> <%= toggle_location_link(@location) %> <% end %> diff --git a/app/views/logs/_tasklist.html.erb b/app/views/logs/_tasklist.html.erb index e2f977a70..7a5e26a0e 100644 --- a/app/views/logs/_tasklist.html.erb +++ b/app/views/logs/_tasklist.html.erb @@ -8,19 +8,21 @@ <% if section.description %>

<%= section.description.html_safe %>

<% end %> - + <%= govuk_task_list(id_prefix: "logs", classes: "app-task-list__items") do |task_list| + section.subsections.each do |subsection| + next unless subsection.displayed_in_tasklist?(@log) && (subsection.applicable_questions(@log).count.positive? || !subsection.enabled?(@log)) + + subsection_status = subsection.status(@log) + task_list.with_item(classes: "#{tasklist_link_class(subsection_status)} app-task-list__item") do |item| + item.with_title(text: subsection.label, href: subsection_href(subsection, @log, current_user), classes: "app-task-list__name-and-hint--my-modifier") + if status_tag(subsection_status, "app-task-list__tag").present? + item.with_status(text: status_tag(subsection_status), classes: "app-task-list__tag") + else + item.with_status(text: status_text(subsection_status), classes: "app-task-list__tag") + end + end + end + end %> <% end %> diff --git a/app/views/merge_requests/_notification_banners.html.erb b/app/views/merge_requests/_notification_banners.html.erb index 38c05dbcd..9e6a085ca 100644 --- a/app/views/merge_requests/_notification_banners.html.erb +++ b/app/views/merge_requests/_notification_banners.html.erb @@ -19,3 +19,11 @@ No changes have been made. Try beginning the merge again. <% end %> <% end %> + +<% if @merge_request.merge_date&.future? %> + <%= govuk_notification_banner(title_text: "Important") do %> +

+ This merge is happening in the future. Wait until the merge date to begin this merge. +

+ <% end %> +<% end %> diff --git a/app/views/merge_requests/helpdesk_ticket.html.erb b/app/views/merge_requests/helpdesk_ticket.html.erb index 4ebd11395..9ebed7a90 100644 --- a/app/views/merge_requests/helpdesk_ticket.html.erb +++ b/app/views/merge_requests/helpdesk_ticket.html.erb @@ -7,14 +7,27 @@ <%= form_with model: @merge_request, url: submit_merge_request_url(request.query_parameters["referrer"]), method: :patch do |f| %> <%= f.govuk_error_summary %> -

Which helpdesk ticket reported this merge?

-

If this merge was reported via a helpdesk ticket, provide the ticket number.
The ticket will be linked to the merge request for reference.

-
- <%= f.govuk_text_field :helpdesk_ticket, caption: { text: "Ticket number", class: "govuk-label govuk-label--s" }, label: { text: "For example, MSD-12345", class: "app-!-colour-muted" } %> + <%= f.govuk_radio_buttons_fieldset :has_helpdesk_ticket, + legend: { text: "Was this merge reported by a helpdesk ticket?", size: "l" } do %> + + <%= f.govuk_radio_button "has_helpdesk_ticket", + true, + label: { text: "Yes" }, + **basic_conditional_html_attributes({ "helpdesk_ticket" => [true] }, "merge_request") do %> + <%= f.govuk_text_field :helpdesk_ticket, + caption: { text: "Ticket number", class: "govuk-label govuk-label--s" }, + label: { text: "For example, MSD-12345", class: "app-!-colour-muted" } %> + <% end %> + + <%= f.govuk_radio_button "has_helpdesk_ticket", + false, + label: { text: "No" } %> + <% end %> + <%= f.hidden_field :page, value: "helpdesk_ticket" %>
<%= f.govuk_submit submit_merge_request_button_text(request.query_parameters["referrer"]) %> diff --git a/app/views/merge_requests/show.html.erb b/app/views/merge_requests/show.html.erb index 0fbde7621..040cd7704 100644 --- a/app/views/merge_requests/show.html.erb +++ b/app/views/merge_requests/show.html.erb @@ -12,7 +12,7 @@ <% unless @merge_request.status == "request_merged" || @merge_request.status == "processing" %>
- <%= govuk_button_link_to "Begin merge", merge_start_confirmation_merge_request_path(@merge_request), disabled: @merge_request.status != "ready_to_merge" %> + <%= govuk_button_link_to "Begin merge", merge_start_confirmation_merge_request_path(@merge_request), disabled: begin_merge_disabled?(@merge_request) %> <%= govuk_button_link_to "Delete merge request", delete_confirmation_merge_request_path(@merge_request), warning: true %>
<% end %> diff --git a/app/views/schemes/details.html.erb b/app/views/schemes/details.html.erb index cb29a56dc..4b23ab016 100644 --- a/app/views/schemes/details.html.erb +++ b/app/views/schemes/details.html.erb @@ -49,11 +49,13 @@ :description, legend: { text: "Is this scheme registered under the Care Standards Act 2000?", size: "m" } %> - <% if current_user.data_coordinator? && current_user.organisation.stock_owners.count.zero? && !current_user.organisation.has_recent_absorbed_organisations? %> + <% scheme_owning_organisation_options = owning_organisation_options(current_user) %> + + <% if scheme_owning_organisation_options.count == 1 %> <%= f.hidden_field :owning_organisation_id, value: current_user.organisation.id %> <% else %> <%= f.govuk_collection_select :owning_organisation_id, - owning_organisation_options(current_user), + scheme_owning_organisation_options, :id, :name, label: { text: "Which organisation owns the housing stock for this scheme?", size: "m" }, diff --git a/app/views/schemes/show.html.erb b/app/views/schemes/show.html.erb index 6cefa5847..0aa25affc 100644 --- a/app/views/schemes/show.html.erb +++ b/app/views/schemes/show.html.erb @@ -52,7 +52,7 @@
-<% if @scheme.owning_organisation.active? && SchemePolicy.new(current_user, @scheme).deactivate? %> +<% if @scheme.owning_organisation.status == :active && SchemePolicy.new(current_user, @scheme).deactivate? %> <%= toggle_scheme_link(@scheme) %> <% end %> diff --git a/config/environments/development.rb b/config/environments/development.rb index 7e1890b02..f75643524 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -81,6 +81,8 @@ Rails.application.configure do # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true + Faker::Config.locale = "en-GB" + # see https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017 config.active_record.yaml_column_permitted_classes = [Time, BigDecimal] diff --git a/config/environments/review.rb b/config/environments/review.rb index f7438fdb6..471879b27 100644 --- a/config/environments/review.rb +++ b/config/environments/review.rb @@ -125,6 +125,8 @@ Rails.application.configure do # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + Faker::Config.locale = "en-GB" + # see https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017 config.active_record.yaml_column_permitted_classes = [Time, BigDecimal] end diff --git a/config/locales/en.yml b/config/locales/en.yml index f8bb8255b..851a9ea2c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -183,8 +183,15 @@ en: merge_date: blank: "Enter a merge date." invalid: "Enter a valid merge date." + more_than_year_from_today: "The merge date must not be later than a year from today’s date." existing_absorbing_organisation: blank: "You must answer absorbing organisation already active?" + merging_organisation_id: + part_of_another_merge: "Another merge request records %{organisation} as merging into %{absorbing_organisation} on %{merge_date}. Select another organisation or remove this organisation from the other merge request." + has_helpdesk_ticket: + blank: "You must answer was this merge reported by a helpdesk ticket?" + helpdesk_ticket: + blank: "You must answer the ticket number" notification: attributes: title: @@ -354,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." @@ -370,6 +379,7 @@ en: during_deactivated_period: "The location is already deactivated during this date, please enter a different date." merge_request: organisation_part_of_another_merge: "This organisation is part of another merge - select a different one." + organisation_part_of_another_incomplete_merge: "Another merge request records %{organisation} as merging into %{absorbing_organisation} on %{merge_date}. Select another organisation or remove this organisation from the other merge request." organisation_not_selected: "Select an organisation from the search list." soft_validations: diff --git a/config/locales/forms/2023/sales/soft_validations.en.yml b/config/locales/forms/2023/sales/soft_validations.en.yml index 20f131e90..abe77ccef 100644 --- a/config/locales/forms/2023/sales/soft_validations.en.yml +++ b/config/locales/forms/2023/sales/soft_validations.en.yml @@ -18,7 +18,7 @@ en: question_text: "Are you sure this person is retired?" title_text: "You told us this person is aged %{age} years and retired." informative_text: "The minimum expected retirement age in England is 66." - + old_persons_shared_ownership_value_check: page_header: "" check_answer_label: "Shared ownership confirmation" @@ -28,28 +28,28 @@ en: joint_purchase: "You told us the buyers are using the Older Persons Shared Ownership scheme." not_joint_purchase: "You told us the buyer is using the Older Persons Shared Ownership scheme." informative_text: "At least one buyer must be aged 65 years and over to use this scheme." - + income1_value_check: check_answer_label: "Buyer 1 income confirmation" hint_text: "" question_text: "Are you sure this is correct?" - min: + ecstat: page_header: "" title_text: "You told us income was %{income}." informative_text: "This is less than we would expect for someone in this working situation." - max: + discounted: page_header: "" title_text: "You told us the income of buyer 1 is %{income}. This seems high. Are you sure this is correct?" - + income2_value_check: check_answer_label: "Buyer 2 income confirmation" hint_text: "" question_text: "Are you sure this is correct?" - min: + ecstat: page_header: "" title_text: "You told us income was %{income}." informative_text: "This is less than we would expect for someone in this working situation." - max: + discounted: page_header: "" title_text: "You told us the income of buyer 2 is %{income}. This seems high. Are you sure this is correct?" @@ -110,7 +110,7 @@ en: hint_text: "" question_text: "Are you sure?" title_text: "You told us practical completion or handover date is more than 3 years before sale completion date." - + value_value_check: page_header: "" check_answer_label: "Purchase price confirmation" @@ -141,14 +141,14 @@ en: question_text: "Are you sure that the deposit is this much higher than the buyer's savings?" title_text: "You told us the buyer’s deposit was %{deposit} and their savings were %{savings}." informative_text: "The deposit amount is higher than we would expect for the amount of savings they have." - + wheel_value_check: page_header: "" check_answer_label: "Does anyone in the household use a wheelchair?" hint_text: "" question_text: "You told us that someone in the household uses a wheelchair." title_text: "You told us that someone in the household uses a wheelchair." - + buyer_livein_value_check: buyer1: page_header: "" @@ -164,7 +164,7 @@ en: question_text: "Are you sure this is correct?" title_text: "You told us that buyer 2 will not live in the property." informative_text: "For %{ownership_scheme} types, the buyer usually lives in the property." - + student_not_child_value_check: page_header: "" check_answer_label: "Student not a child confirmation" @@ -172,7 +172,7 @@ en: question_text: "Are you sure this person is not a child?" title_text: "You told us this person is a student aged between 16 and 19." informative_text: "Are you sure this person is not a child?" - + partner_under_16_value_check: page_header: "" check_answer_label: "Partner under 16 confirmation" @@ -180,7 +180,7 @@ en: question_text: "Are you sure this is correct?" title_text: "You told us this person is aged %{age} years and has 'Partner' relationship to buyer 1." informative_text: "Are you sure this is correct?" - + multiple_partners_value_check: page_header: "" check_answer_label: "Multiple partners confirmation" @@ -196,7 +196,7 @@ en: question_text: "Are you sure this is correct?" title_text: "You told us that the monthly charges were %{mscharge}." informative_text: "This is higher than we would expect." - + extra_borrowing_value_check: page_header: "" check_answer_label: "Extra borrowing confirmation" diff --git a/config/locales/forms/2024/lettings/setup.en.yml b/config/locales/forms/2024/lettings/setup.en.yml index 5a18fe719..68bc95364 100644 --- a/config/locales/forms/2024/lettings/setup.en.yml +++ b/config/locales/forms/2024/lettings/setup.en.yml @@ -30,7 +30,7 @@ en: scheme_id: page_header: "Scheme" check_answer_label: "Scheme name" - hint_text: "Enter postcode or scheme name.

A supported housing scheme provides shared or self-contained housing for a particular client group, for example younger or vulnerable people." + hint_text: "Enter postcode, scheme name, or scheme code (for example, S123).

A supported housing scheme provides shared or self-contained housing for a particular client group, for example younger or vulnerable people." question_text: "What scheme is this log for?" location_id: diff --git a/config/locales/forms/2024/sales/soft_validations.en.yml b/config/locales/forms/2024/sales/soft_validations.en.yml index b9b1ad479..fa5434311 100644 --- a/config/locales/forms/2024/sales/soft_validations.en.yml +++ b/config/locales/forms/2024/sales/soft_validations.en.yml @@ -31,11 +31,11 @@ en: check_answer_label: "Buyer 1 income confirmation" hint_text: "" question_text: "Are you sure this is correct?" - min: + ecstat: page_header: "" title_text: "You told us income was %{income}." - informative_text: "This is less than we would expect for someone in this working situation." - max: + informative_text: "This is %{more_or_less} than we would expect for someone in this working situation." + discounted: page_header: "" title_text: "You told us the income of buyer 1 is %{income}. This seems high. Are you sure this is correct?" @@ -43,11 +43,11 @@ en: check_answer_label: "Buyer 2 income confirmation" hint_text: "" question_text: "Are you sure this is correct?" - min: + ecstat: page_header: "" title_text: "You told us income was %{income}." - informative_text: "This is less than we would expect for someone in this working situation." - max: + informative_text: "This is %{more_or_less} than we would expect for someone in this working situation." + discounted: page_header: "" title_text: "You told us the income of buyer 2 is %{income}. This seems high. Are you sure this is correct?" @@ -108,7 +108,7 @@ en: hint_text: "" question_text: "Are you sure?" title_text: "You told us practical completion or handover date is more than 3 years before sale completion date." - + value_value_check: page_header: "" check_answer_label: "Purchase price confirmation" @@ -202,7 +202,7 @@ en: question_text: "Are you sure this is correct?" title_text: "You told us that the monthly charges were %{mscharge}." informative_text: "This is higher than we would expect." - + extra_borrowing_value_check: page_header: "" check_answer_label: "Extra borrowing confirmation" diff --git a/config/locales/forms/2025/sales/sale_information.en.yml b/config/locales/forms/2025/sales/sale_information.en.yml index 33826e58b..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 stake" - hint_text: "Enter the amount of initial equity held by the purchaser (for example, 25% or 50%)" - question_text: "What was the initial percentage equity stake 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,12 +198,23 @@ 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: - check_answer_label: "Does the property have any monthly leasehold charges?" + check_answer_label: "Does the property have any service charges?" hint_text: "For example, service and management charges" - question_text: "Does the property have any monthly leasehold charges?" + question_text: "Does the property have any service charges?" mscharge: check_answer_label: "Monthly leasehold charges" hint_text: "" @@ -199,3 +243,14 @@ 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: + check_answer_label: "Does the property have an estate management fee?" + hint_text: "Estate management fees are typically used for the maintenance of communal gardens, payments, private roads, car parks and/or play areas within new build estates." + question_text: "Does the property have an estate management fee?" + management_fee: + check_answer_label: "Monthly estate management fee" + hint_text: "" + question_text: "Enter the total monthly management fee" diff --git a/config/locales/forms/2025/sales/soft_validations.en.yml b/config/locales/forms/2025/sales/soft_validations.en.yml index c8f0990ba..11bbefd72 100644 --- a/config/locales/forms/2025/sales/soft_validations.en.yml +++ b/config/locales/forms/2025/sales/soft_validations.en.yml @@ -31,32 +31,32 @@ en: check_answer_label: "Buyer 1 income confirmation" hint_text: "" question_text: "Are you sure this is correct?" - min: + ecstat: page_header: "" - title_text: "You told us income was %{income}." - informative_text: "This is less than we would expect for someone in this working situation." - max: + title_text: "You told us the income of buyer 1 is %{income}." + informative_text: "This is %{more_or_less} than we would expect for someone in this working situation." + discounted: page_header: "" - title_text: "You told us the income of buyer 1 is %{income}. This seems high. Are you sure this is correct?" + title_text: "You told us the income of buyer 1 is %{income}. This seems high for this sale type. Are you sure this is correct?" income2_value_check: check_answer_label: "Buyer 2 income confirmation" hint_text: "" question_text: "Are you sure this is correct?" - min: + ecstat: page_header: "" - title_text: "You told us income was %{income}." - informative_text: "This is less than we would expect for someone in this working situation." - max: + title_text: "You told us the income of buyer 2 is %{income}." + informative_text: "This is %{more_or_less} than we would expect for someone in this working situation." + discounted: page_header: "" - title_text: "You told us the income of buyer 2 is %{income}. This seems high. Are you sure this is correct?" + title_text: "You told us the income of buyer 2 is %{income}. This seems high for this sale type. Are you sure this is correct?" combined_income_value_check: page_header: "" check_answer_label: "Combined income confirmation" hint_text: "" question_text: "Are you sure this is correct?" - title_text: "You told us the combined income of this household is %{combined_income}. This seems high. Are you sure this is correct?" + title_text: "You told us the combined income of this household is %{combined_income}. This seems high for this sale type. Are you sure this is correct?" mortgage_value_check: page_header: "" 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/config/routes.rb b/config/routes.rb index 6ac7b3f34..55d58b41b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -382,6 +382,18 @@ Rails.application.routes.draw do end end + resources :csv_downloads, path: "csv-downloads" do + member do + get "/", to: "csv_downloads#show", as: "show" + get "download", to: "csv_downloads#download" + end + end + + get "create-test-lettings-log", to: "lettings_logs#create_test_log" + get "create-test-sales-log", to: "sales_logs#create_test_log" + get "create-setup-test-lettings-log", to: "lettings_logs#create_setup_test_log" + get "create-setup-test-sales-log", to: "sales_logs#create_setup_test_log" + scope via: :all do match "/404", to: "errors#not_found" match "/429", to: "errors#too_many_requests", status: 429 diff --git a/db/migrate/20241114154215_add_management_fee_fields.rb b/db/migrate/20241114154215_add_management_fee_fields.rb new file mode 100644 index 000000000..f8455d259 --- /dev/null +++ b/db/migrate/20241114154215_add_management_fee_fields.rb @@ -0,0 +1,8 @@ +class AddManagementFeeFields < ActiveRecord::Migration[7.0] + def change + change_table :sales_logs, bulk: true do |t| + t.column :has_management_fee, :integer + t.column :management_fee, :decimal, precision: 10, scale: 2 + end + end +end 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/migrate/20241118104046_add_csv_download_table.rb b/db/migrate/20241118104046_add_csv_download_table.rb new file mode 100644 index 000000000..9b4f73f0b --- /dev/null +++ b/db/migrate/20241118104046_add_csv_download_table.rb @@ -0,0 +1,12 @@ +class AddCsvDownloadTable < ActiveRecord::Migration[7.0] + def change + create_table :csv_downloads do |t| + t.column :download_type, :string + t.column :filename, :string + t.column :expiration_time, :integer + t.timestamps + t.references :user + t.references :organisation + end + end +end diff --git a/db/migrate/20241122154743_add_has_helpdest_ticket.rb b/db/migrate/20241122154743_add_has_helpdest_ticket.rb new file mode 100644 index 000000000..103611ad8 --- /dev/null +++ b/db/migrate/20241122154743_add_has_helpdest_ticket.rb @@ -0,0 +1,5 @@ +class AddHasHelpdestTicket < ActiveRecord::Migration[7.0] + def change + add_column :merge_requests, :has_helpdesk_ticket, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index ef635628c..0cdc15e9f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_10_31_102744) do +ActiveRecord::Schema[7.0].define(version: 2024_11_22_154743) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -64,6 +64,18 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_31_102744) do t.datetime "discarded_at" end + create_table "csv_downloads", force: :cascade do |t| + t.string "download_type" + t.string "filename" + t.integer "expiration_time" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "user_id" + t.bigint "organisation_id" + t.index ["organisation_id"], name: "index_csv_downloads_on_organisation_id" + t.index ["user_id"], name: "index_csv_downloads_on_user_id" + end + create_table "csv_variable_definitions", force: :cascade do |t| t.string "variable", null: false t.string "definition", null: false @@ -466,6 +478,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_31_102744) do t.boolean "request_merged" t.boolean "processing" t.boolean "existing_absorbing_organisation" + t.boolean "has_helpdesk_ticket" end create_table "notifications", force: :cascade do |t| @@ -746,6 +759,13 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_31_102744) do t.integer "partner_under_16_value_check" t.integer "multiple_partners_value_check" 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/db/seeds.rb b/db/seeds.rb index b58f7e0a8..2f018be91 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,434 +1,175 @@ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Examples: -# -# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) -# Character.create(name: 'Luke', movie: movies.first) -# rubocop:disable Rails/Output -def create_data_protection_confirmation(user) - DataProtectionConfirmation.find_or_create_by!( - organisation: user.organisation, - confirmed: true, - data_protection_officer: user, - signed_at: Time.zone.local(2019, 1, 1), - data_protection_officer_email: user.email, - data_protection_officer_name: user.name, - ) +def find_or_create_user(organisation, email, name, role) + case role + when :data_provider + FactoryBot.create(:user, :if_unique, :data_provider, organisation:, email:, name:, password: ENV["REVIEW_APP_USER_PASSWORD"]) + when :data_coordinator + FactoryBot.create(:user, :if_unique, :data_coordinator, organisation:, email:, name:, password: ENV["REVIEW_APP_USER_PASSWORD"]) + when :support + FactoryBot.create(:user, :if_unique, :support, organisation:, email:, name:, password: ENV["REVIEW_APP_USER_PASSWORD"]) + end end unless Rails.env.test? - stock_owner1 = Organisation.find_or_create_by!( - name: "Stock Owner 1", - address_line1: "2 Marsham Street", - address_line2: "London", - postcode: "SW1P 4DF", - holds_own_stock: true, - other_stock_owners: "None", - managing_agents_label: "None", - provider_type: "LA", - ) - stock_owner2 = Organisation.find_or_create_by!( - name: "Stock Owner 2", - address_line1: "2 Marsham Street", - address_line2: "London", - postcode: "SW1P 4DF", - holds_own_stock: true, - other_stock_owners: "None", - managing_agents_label: "None", - provider_type: "LA", - ) - managing_agent1 = Organisation.find_or_create_by!( - name: "Managing Agent 1 (PRP)", - address_line1: "2 Marsham Street", - address_line2: "London", - postcode: "SW1P 4DF", - holds_own_stock: true, - other_stock_owners: "None", - managing_agents_label: "None", - provider_type: "PRP", - ) - managing_agent2 = Organisation.find_or_create_by!( - name: "Managing Agent 2", - address_line1: "2 Marsham Street", - address_line2: "London", - postcode: "SW1P 4DF", - holds_own_stock: true, - other_stock_owners: "None", - managing_agents_label: "None", - provider_type: "LA", - ) + if LocalAuthority.count.zero? + la_path = "config/local_authorities_data/initial_local_authorities.csv" + service = Imports::LocalAuthoritiesService.new(path: la_path) + service.call + end - org = Organisation.find_or_create_by!( - name: "MHCLG", - address_line1: "2 Marsham Street", - address_line2: "London", - postcode: "SW1P 4DF", - holds_own_stock: true, - other_stock_owners: "None", - managing_agents_label: "None", - provider_type: "LA", - ) do - info = "Seeded MHCLG Organisation" - if Rails.env.development? - pp info - else - Rails.logger.info info + if LaRentRange.count.zero? + Dir.glob("config/rent_range_data/*.csv").each do |path| + start_year = File.basename(path, ".csv") + service = Imports::RentRangesService.new(start_year:, path:) + service.call end end - standalone_owns_stock = Organisation.find_or_create_by!( - name: "Standalone Owns Stock 1 Ltd", - address_line1: "2 Marsham Street", - address_line2: "London", - postcode: "SW1P 4DF", - holds_own_stock: true, - other_stock_owners: "None", - managing_agents_label: "None", - provider_type: "LA", - ) + if LaSaleRange.count.zero? + Dir.glob("config/sale_range_data/*.csv").each do |path| + start_year = File.basename(path, ".csv") + service = Imports::SaleRangesService.new(start_year:, path:) + service.call + end + end + + first_run = Organisation.count.zero? + + all_rent_periods = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - standalone_no_stock = Organisation.find_or_create_by!( - name: "Standalone No Stock 1 Ltd", + mhclg = FactoryBot.create( + :organisation, + :if_unique, + name: "MHCLG", address_line1: "2 Marsham Street", address_line2: "London", postcode: "SW1P 4DF", - holds_own_stock: false, + holds_own_stock: true, other_stock_owners: "None", managing_agents_label: "None", provider_type: "LA", - ) - - User.find_or_create_by!( - name: "Provider Owns Stock", - email: "provider.owner1@example.com", - organisation: standalone_owns_stock, - role: "data_provider", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end - - User.find_or_create_by!( - name: "Coordinator Owns Stock", - email: "coordinator.owner1@example.com", - organisation: standalone_owns_stock, - role: "data_coordinator", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end - - User.find_or_create_by!( - name: "Provider No Stock", - email: "provider.nostock@example.com", - organisation: standalone_no_stock, - role: "data_provider", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end - - User.find_or_create_by!( - name: "Coordinator No Stock", - email: "coordinator.nostock@example.com", - organisation: standalone_no_stock, - role: "data_coordinator", - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - end - - User.find_or_create_by!( - name: "Stock owner 1", - email: "stock_owner1_dpo@example.com", - organisation: stock_owner1, - role: "data_coordinator", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end - - User.find_or_create_by!( - name: "Stock owner 2", - email: "stock_owner2_dpo@example.com", - organisation: stock_owner2, - role: "data_coordinator", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end - - User.find_or_create_by!( - name: "Managing agent 1", - email: "managing_agent1_dpo@example.com", - organisation: managing_agent1, - role: "data_coordinator", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end - - User.find_or_create_by!( - name: "Managing agent 2", - email: "managing_agent2_dpo@example.com", - organisation: managing_agent2, - role: "data_coordinator", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end - - OrganisationRelationship.find_or_create_by!( - parent_organisation: stock_owner1, - child_organisation: org, - ) - OrganisationRelationship.find_or_create_by!( - parent_organisation: stock_owner2, - child_organisation: org, - ) - OrganisationRelationship.find_or_create_by!( - parent_organisation: org, - child_organisation: managing_agent1, - ) - OrganisationRelationship.find_or_create_by!( - parent_organisation: org, - child_organisation: managing_agent2, + housing_registration_no: nil, + rent_periods: all_rent_periods, ) if Rails.env.development? || Rails.env.review? - User.find_or_create_by!( - name: "Provider", - email: "provider@example.com", - organisation: org, - role: "data_provider", - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - end + stock_owner1 = FactoryBot.create(:organisation, :if_unique, :la, :holds_own_stock, name: "Stock Owner 1", rent_periods: all_rent_periods) + stock_owner2 = FactoryBot.create(:organisation, :if_unique, :la, :holds_own_stock, name: "Stock Owner 2", rent_periods: all_rent_periods.sample(5)) + managing_agent1 = FactoryBot.create(:organisation, :if_unique, :prp, :holds_own_stock, name: "Managing Agent 1 (PRP)", rent_periods: all_rent_periods) + managing_agent2 = FactoryBot.create(:organisation, :if_unique, :la, :holds_own_stock, name: "Managing Agent 2", rent_periods: all_rent_periods.sample(5)) + standalone_owns_stock = FactoryBot.create(:organisation, :if_unique, :la, :holds_own_stock, name: "Standalone Owns Stock 1 Ltd", rent_periods: all_rent_periods) + standalone_no_stock = FactoryBot.create(:organisation, :if_unique, :la, :does_not_own_stock, name: "Standalone No Stock 1 Ltd", rent_periods: all_rent_periods) + + other_orgs = FactoryBot.create_list(:organisation, 5, :prp, rent_periods: all_rent_periods.sample(3)) if first_run + + OrganisationRelationship.find_or_create_by!( + parent_organisation: stock_owner1, + child_organisation: mhclg, + ) + OrganisationRelationship.find_or_create_by!( + parent_organisation: stock_owner2, + child_organisation: mhclg, + ) + OrganisationRelationship.find_or_create_by!( + parent_organisation: mhclg, + child_organisation: managing_agent1, + ) + OrganisationRelationship.find_or_create_by!( + parent_organisation: mhclg, + child_organisation: managing_agent2, + ) - User.find_or_create_by!( - name: "Coordinator", - email: "coordinator@example.com", - organisation: org, - role: "data_coordinator", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - user.is_dpo = true - create_data_protection_confirmation(user) - end + provider = find_or_create_user(mhclg, "provider@example.com", "Provider", :data_provider) + coordinator = find_or_create_user(mhclg, "coordinator@example.com", "Coordinator", :data_coordinator) + support = find_or_create_user(mhclg, "support@example.com", "Support", :support) - support_user = User.find_or_create_by!( - name: "Support", - email: "support@example.com", - organisation: org, - role: "support", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) - end + stock_owner1_user = find_or_create_user(stock_owner1, "stock_owner1_dpo@example.com", "Stock owner 1", :data_coordinator) + stock_owner2_user = find_or_create_user(stock_owner2, "stock_owner2_dpo@example.com", "Stock owner 2", :data_coordinator) - pp "Seeded dummy users" - end + managing_agent1_user = find_or_create_user(managing_agent1, "managing_agent1_dpo@example.com", "Managing agent 1", :data_coordinator) + managing_agent2_user = find_or_create_user(managing_agent2, "managing_agent2_dpo@example.com", "Managing agent 2", :data_coordinator) - if (Rails.env.development? || Rails.env.review?) && SalesLog.count.zero? - SalesLog.find_or_create_by!( - assigned_to: support_user, - owning_organisation: org, - managing_organisation: org, - saledate: Time.zone.today, - purchid: "1", - ownershipsch: 1, - type: 2, - jointpur: 1, - jointmore: 1, - ) + provider_owner1 = find_or_create_user(standalone_owns_stock, "provider.owner1@example.com", "Provider Owns Stock", :data_provider) + coordinator_owner1 = find_or_create_user(standalone_owns_stock, "coordinator.owner1@example.com", "Coordinator Owns Stock", :data_coordinator) - SalesLog.find_or_create_by!( - assigned_to: support_user, - owning_organisation: org, - managing_organisation: org, - saledate: Time.zone.today, - purchid: "1", - ownershipsch: 2, - type: 9, - jointpur: 1, - jointmore: 1, - ) + find_or_create_user(standalone_no_stock, "provider.nostock@example.com", "Provider No Stock", :data_provider) + find_or_create_user(standalone_no_stock, "coordinator.nostock@example.com", "Coordinator No Stock", :data_coordinator) - SalesLog.find_or_create_by!( - assigned_to: support_user, - owning_organisation: org, - managing_organisation: org, - saledate: Time.zone.today, - purchid: "1", - ownershipsch: 3, - type: 10, - companybuy: 1, - ) + if Scheme.count.zero? + beulahside = FactoryBot.create(:scheme, service_name: "Beulahside Care", owning_organisation: mhclg) + abdullah = FactoryBot.create(:scheme, service_name: "Abdullahview Point", owning_organisation: mhclg) - pp "Seeded a sales log of each type" - end + FactoryBot.create(:location, scheme: beulahside, name: "Rectory Road", postcode: "CU193AA", location_code: "E09000033", location_admin_district: "Westminster", type_of_unit: 4, mobility_type: "N") + FactoryBot.create(:location, scheme: beulahside, name: "Smithy Lane", postcode: "DM25ODC", location_code: "E09000033", location_admin_district: "Westminster", type_of_unit: 1, mobility_type: "W") + FactoryBot.create(:location, scheme: abdullah, name: "Smithy Lane", postcode: "YX130WP", location_code: "E09000033", location_admin_district: "Westminster", type_of_unit: 2, mobility_type: "W") - if Rails.env.development? || Rails.env.review? - dummy_org = Organisation.find_or_create_by!( - name: "FooBar LTD", - address_line1: "Higher Kingston", - address_line2: "Yeovil", - postcode: "BA21 4AT", - holds_own_stock: true, - other_stock_owners: "None", - managing_agents_label: "None", - provider_type: "LA", - ) + mhclg_scheme = FactoryBot.create(:scheme, :created_now, owning_organisation: mhclg) + stock_owner_scheme = FactoryBot.create(:scheme, owning_organisation: stock_owner1) - pp "Seeded dummy FooBar LTD organisation" + other_schemes = first_run ? other_orgs.sample(3).map { |org| FactoryBot.create(:scheme, owning_organisation: org) } : [] - User.find_or_create_by!( - name: "Dummy user", - email: "dummy_org@example.com", - organisation: dummy_org, - role: "data_provider", - is_dpo: true, - ) do |user| - user.password = ENV["REVIEW_APP_USER_PASSWORD"] - user.confirmed_at = Time.zone.now - create_data_protection_confirmation(user) + [mhclg_scheme, stock_owner_scheme, *other_schemes].each do |scheme| + FactoryBot.create(:location, scheme:) + end + [beulahside, mhclg_scheme, *other_schemes].each do |scheme| + FactoryBot.create_list(:location, 3, scheme:) + end end - end - if (Rails.env.development? || Rails.env.review?) && Scheme.count.zero? - scheme1 = Scheme.create!( - service_name: "Beulahside Care", - sensitive: 0, - registered_under_care_act: 1, - support_type: 2, - scheme_type: 4, - intended_stay: "M", - primary_client_group: "O", - has_other_client_group: 1, - secondary_client_group: "H", - owning_organisation: org, - arrangement_type: "D", - confirmed: true, - created_at: Time.zone.now, - ) + other_org_users = first_run ? other_orgs.map { |org| org.users.first } : [] + users_with_logs = [provider, coordinator, support, stock_owner1_user, stock_owner2_user, managing_agent1_user, managing_agent2_user, provider_owner1, coordinator_owner1, *other_org_users] - scheme2 = Scheme.create!( - service_name: "Abdullahview Point", - sensitive: 0, - registered_under_care_act: 1, - support_type: 2, - scheme_type: 5, - intended_stay: "S", - primary_client_group: "D", - secondary_client_group: "E", - has_other_client_group: 1, - owning_organisation: org, - arrangement_type: "D", - confirmed: true, - created_at: Time.zone.now, - ) + if SalesLog.count.zero? + users_with_logs.each do |user| + FactoryBot.create(:sales_log, :shared_ownership_setup_complete, assigned_to: user) + FactoryBot.create(:sales_log, :discounted_ownership_setup_complete, assigned_to: user) + FactoryBot.create(:sales_log, :outright_sale_setup_complete, assigned_to: user) if Time.zone.today < Time.zone.local(2025, 4, 1) + FactoryBot.create(:sales_log, :completed, assigned_to: user) + FactoryBot.create_list(:sales_log, 2, :completed, :ignore_validation_errors, saledate: Time.zone.today - 1.year, assigned_to: user) - Scheme.create!( - service_name: "Caspermouth Center", - sensitive: 1, - registered_under_care_act: 1, - support_type: 4, - scheme_type: 7, - intended_stay: "X", - primary_client_group: "G", - has_other_client_group: 1, - secondary_client_group: "R", - owning_organisation: dummy_org, - arrangement_type: "D", - confirmed: true, - created_at: Time.zone.now, - ) + next unless FeatureToggle.allow_future_form_use? - Location.create!( - scheme: scheme1, - location_code: "E09000033", - location_admin_district: "Westminster", - postcode: "CU193AA", - name: "Rectory Road", - type_of_unit: 4, - units: 1, - mobility_type: "N", - ) + FactoryBot.create(:sales_log, :shared_ownership_setup_complete, saledate: Time.zone.today + 1.year, assigned_to: user) + FactoryBot.create(:sales_log, :discounted_ownership_setup_complete, saledate: Time.zone.today + 1.year, assigned_to: user) + FactoryBot.create(:sales_log, :completed, saledate: Time.zone.today + 1.year, assigned_to: user) + end - Location.create!( - scheme: scheme1, - location_code: "E09000033", - location_admin_district: "Westminster", - postcode: "DM250DC", - name: "Smithy Lane", - type_of_unit: 1, - units: 1, - mobility_type: "W", - ) - - Location.create!( - scheme: scheme2, - location_code: "E09000033", - location_admin_district: "Westminster", - postcode: "YX130WP", - name: "Smithy Lane", - type_of_unit: 2, - units: 1, - mobility_type: "W", - ) - pp "Seeded dummy schemes" - end - - if LocalAuthority.count.zero? - la_path = "config/local_authorities_data/initial_local_authorities.csv" - service = Imports::LocalAuthoritiesService.new(path: la_path) - service.call - end - - if (Rails.env.development? || Rails.env.review?) && LocalAuthorityLink.count.zero? - links_data_paths = ["config/local_authorities_data/local_authority_links_2023.csv", "config/local_authorities_data/local_authority_links_2022.csv"] - links_data_paths.each do |path| - service = Imports::LocalAuthorityLinksService.new(path:) - service.call + FactoryBot.create(:sales_log, :completed, assigned_to: managing_agent1_user, owning_organisation: mhclg) + FactoryBot.create(:sales_log, :completed, assigned_to: provider, owning_organisation: stock_owner1) end - pp "Seeded local authority links" - end - - if LaRentRange.count.zero? - Dir.glob("config/rent_range_data/*.csv").each do |path| - start_year = File.basename(path, ".csv") - service = Imports::RentRangesService.new(start_year:, path:) - service.call + if LettingsLog.count.zero? + users_with_logs.each do |user| + FactoryBot.create(:lettings_log, :setup_completed, assigned_to: user) + FactoryBot.create(:lettings_log, :completed, assigned_to: user) + if user.organisation.owned_schemes.any? + scheme = user.organisation.owned_schemes.first + FactoryBot.create(:lettings_log, :setup_completed, :sh, scheme:, location: scheme.locations.first, assigned_to: user) + end + FactoryBot.create_list(:lettings_log, 2, :completed, :ignore_validation_errors, startdate: Time.zone.today - 1.year, assigned_to: user) + + next unless FeatureToggle.allow_future_form_use? + + FactoryBot.create(:lettings_log, :setup_completed, startdate: Time.zone.today + 1.year, assigned_to: user) + FactoryBot.create(:lettings_log, :completed, startdate: Time.zone.today + 1.year, assigned_to: user) + if user.organisation.owned_schemes.any? + scheme = user.organisation.owned_schemes.first + FactoryBot.create(:lettings_log, :setup_completed, :sh, scheme:, location: scheme.locations.first, startdate: Time.zone.today + 1.year, assigned_to: user) + end + end + + FactoryBot.create(:lettings_log, :completed, assigned_to: managing_agent1_user, owning_organisation: mhclg) + FactoryBot.create(:lettings_log, :completed, assigned_to: provider, owning_organisation: stock_owner1) end - end - if LaSaleRange.count.zero? - Dir.glob("config/sale_range_data/*.csv").each do |path| - start_year = File.basename(path, ".csv") - service = Imports::SaleRangesService.new(start_year:, path:) - service.call + if LocalAuthorityLink.count.zero? + links_data_paths = ["config/local_authorities_data/local_authority_links_2023.csv", "config/local_authorities_data/local_authority_links_2022.csv"] + links_data_paths.each do |path| + service = Imports::LocalAuthorityLinksService.new(path:) + service.call + end end end end @@ -438,4 +179,3 @@ if LocalAuthority.count.zero? service = Imports::LocalAuthoritiesService.new(path:) service.call end -# rubocop:enable Rails/Output diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index f8962318e..d102b14ef 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -255,6 +255,7 @@ GEM PLATFORMS arm64-darwin-21 + arm64-darwin-23 x86_64-darwin-22 x86_64-linux diff --git a/docs/app_api.md b/docs/app_api.md index e1987a897..627be0ad7 100644 --- a/docs/app_api.md +++ b/docs/app_api.md @@ -12,4 +12,4 @@ In order to use the app as an API, you will need to configure requests to the AP - `Content-Type = application/json` - `Action = application/json` N.B. If you use `*/*` instead, the request won't be recognised as an API request` -Currently only the logs controller is configured to accept and authenticate API requests, when the above API environment variables are set. +Currently, only the Logs Controller is configured to accept and authenticate API requests, provided that the specified API environment variables are set. Please note that the API has not been actively maintained for an extended period and may not function as expected. Additionally, the required environment variables are not configured on any of the environments deployed on AWS, rendering API requests to those environments non-functional. diff --git a/docs/exports.md b/docs/exports.md index f16efe893..4b971e29a 100644 --- a/docs/exports.md +++ b/docs/exports.md @@ -6,16 +6,14 @@ nav_order: 7 All data collected by the application needs to be exported to the Consolidated Data Store (CDS) which is a data warehouse based on MS SQL running in the DAP (Data Analytics Platform). -This is done via XML exports saved in an S3 bucket located in the DAP VPC using dedicated credentials shared out of band. The data mapping for this export can be found in `app/services/exports/lettings_log_export_service.rb` +This is done via XML exports saved in an S3 bucket. +We currently export lettings logs, users and organisations. +The data mapping for these exports can be found in: + +- Lettings logs `app/services/exports/lettings_log_export_service.rb` +- Organisations `app/services/exports/organisation_export_service.rb` +- Users `app/services/exports/user_export_service.rb` Initially the application database field names and field types were chosen to match the existing CDS data as closely as possible to minimise the amount of transformation needed. This has led to a less than optimal data model though and increasingly we should look to transform at the mapping layer where beneficial for our application. We have a cron job triggering the export service daily at 5am. - -The S3 bucket is located in the DAP VPC rather than the application VPC as DAP runs in an AWS account directly so access to the S3 bucket can be restricted to only the IPs used by the application. This is not possible the other way around as [Gov PaaS does not support restricting S3 access by IP](https://github.com/alphagov/paas-roadmap/issues/107). - -## Other options previously considered - -- CDC replication using a managed service such as [AWS DMS](https://aws.amazon.com/dms/) - - Would require VPC peering which [Gov PaaS does not currently support](https://github.com/alphagov/paas-roadmap/issues/105) - - Would require CDS to make changes to their ingestion model diff --git a/docs/form/builder.md b/docs/form/builder.md index 155abaed6..5d21b1c43 100644 --- a/docs/form/builder.md +++ b/docs/form/builder.md @@ -9,13 +9,11 @@ nav_order: 1 The setup this log section is treated slightly differently from the rest of the form. It is more accurately viewed as providing metadata about the form than as being part of the form itself. It also needs to know far more about the application specific context than other parts of the form such as who the current user is, what organisation they’re part of and what role they have etc. -As a result it’s not modelled as part of the config but rather as code. It still uses the same [Form Runner](/form/runner) components though. - -## Features the Form Config supports +## Features the Form supports - Defining sections, subsections, pages and questions that fit the GOV.UK task list pattern -- Auto-generated routes – URLs are automatically created from dasherized page names +- Auto-generated routes – URLs are automatically created from dasherized page names (ids) - Data persistence requires a database field to exist which matches the name/id for each question (and answer option for checkbox questions) @@ -39,63 +37,84 @@ As a result it’s not modelled as part of the config but rather as code. It sti - For complex HTML guidance partials can be referenced -## JSON Config - -The form for this is driven by a JSON file in `/config/forms/{start_year}_{end_year}.json` - -The JSON should follow the structure: - -```jsonc -{ - "form_type": "lettings" / "sales", - "start_year": Integer, // i.e. 2020 - "end_year": Integer, // i.e. 2021 - "sections": { - "[snake_case_section_name_string]": { - "label": String, - "description": String, - "subsections": { - "[snake_case_subsection_name_string]": { - "label": String, - "pages": { - "[snake_case_page_name_string]": { - "header": String, - "description": String, - "questions": { - "[snake_case_question_name_string]": { - "header": String, - "hint_text": String, - "check_answer_label": String, - "type": "text" / "numeric" / "radio" / "checkbox" / "date", - "min": Integer, // numeric only - "max": Integer, // numeric only - "step": Integer, // numeric only - "width": 2 / 3 / 4 / 5 / 10 / 20, // text and numeric only - "prefix": String, // numeric only - "suffix": String, //numeric only - "answer_options": { // checkbox and radio only - "0": String, - "1": String - }, - "conditional_for": { - "[snake_case_question_to_enable_1_name_string]": ["condition-that-enables"], - "[snake_case_question_to_enable_2_name_string]": ["condition-that-enables"] - }, - "inferred_answers": { "field_that_gets_inferred_from_current_field": { "is_that_field_inferred": true } }, - "inferred_check_answers_value": [{ - "condition": { "field_name_for_inferred_check_answers_condition": "field_value_for_inferred_check_answers_condition" }, - "value": "Inferred value that gets displayed if condition is met" - }] - } - }, - "depends_on": [{ "question_key": "answer_value_required_for_this_page_to_be_shown" }] - } - } - } - } - } - } -} +## Form definition + +The Form should follow the structure: + +``` +SECTIONS = [ + Form::Sales::Sections::Section +].freeze + +Form.new(nil, start_year, SECTIONS, form_type - "lettings" / "sales") + +class Form::Sales::Sections::Section < ::Form::Section + def initialize(id, hsh, form) + super + @id = [snake_case_section_name_string] + @label = [String] + @description = [String] + @subsections = [Form::Sales::Subsections::Subsection.new(nil, nil, self)] + end +end + +class Form::Sales::Subsections::Subsection < ::Form::Subsection + def initialize(id, hsh, section) + super + @id = [snake_case_subsection_name_string] + @label = [String] + @depends_on = [{ "question_key/method_key": "answer_value_required_for_this_subsection_to_be_shown" }] + end + + def pages + @pages ||= [Form::Sales::Pages::Page.new(nil, nil, self),] + end +end + +class Form::Sales::Pages::Page < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = [snake_case_page_name_string] + @header = [String,] + @depends_on = [{ "question_key": "answer_value_required_for_this_page_to_be_shown" }] + end + + def questions + @questions ||= [ + Form::Sales::Questions::Question.new(nil, nil, self), + ] + end +end + +class Form::Sales::Questions::Question < ::Form::Question + def initialize(id, hsh, page) + super + @id = [snake_case_question_name_string] + @hint_text = [String,] + @check_answer_label = [String,] + @type = ["text" / "numeric" / "radio" / "checkbox" / "date",] + @min = [Integer, // numeric only] + @max = [Integer, // numeric only] + @step = [Integer, // numeric only] + @width = [2 / 3 / 4 / 5 / 10 / 20, // text and numeric only] + @prefix = [String, // numeric only] + @suffix = [String, //numeric only] + @answer_options = { // checkbox and radio only + "0": String, + "1": String + }, + @conditional_for = { + "[snake_case_question_to_enable_1_name_string]": ["condition-that-enables"], + "[snake_case_question_to_enable_2_name_string]": ["condition-that-enables"] + }, + @inferred_answers = { "field_that_gets_inferred_from_current_field": { "is_that_field_inferred": true } }, + @inferred_check_answers_value = [{ + "condition": { "field_name_for_inferred_check_answers_condition": "field_value_for_inferred_check_answers_condition" }, + "value": "Inferred value that gets displayed if condition is met" + }] + @question_number = Integer + end +end ``` Assumptions made by the format: @@ -127,47 +146,8 @@ Assumptions made by the format: Form navigation works by stepping sequentially through every page defined in the JSON form definition for the given subsection. For every page it checks if it has "depends_on" conditions. If it does, it evaluates them to determine whether that page should be show or not. -In this way we can build up whole branches by having: - -```jsonc -"page_1": { "questions": { "question_1: "answer_options": ["A", "B"] } }, -"page_2": { "questions": { "question_2: "answer_options": ["C", "D"] }, "depends_on": [{ "question_1": "A" }] }, -"page_3": { "questions": { "question_3: "answer_options": ["E", "F"] }, "depends_on": [{ "question_1": "A" }] }, -"page_4": { "questions": { "question_4: "answer_options": ["G", "H"] }, "depends_on": [{ "question_1": "B" }] }, -``` - -## JSON form validation against Schema - -To validate the form JSON against the schema you can run: - -```bash -rake form_definition:validate["config/forms/2021_2022.json"] -``` - -Note: you may have to escape square brackets in zsh: - -```bash -rake form_definition:validate\["config/forms/2021_2022.json"\] -``` - -This will validate the given form definition against the schema in `config/forms/schema/generic.json`. - -You can also run: - -```bash -rake form_definition:validate_all -``` - -This will validate all forms in directories `["config/forms", "spec/fixtures/forms"]` +We can also define custom `routed_to?` methods on pages for more complex routing logic. ## Form models and definition For information about the form model and related models (section, subsection, page, question) and how these relate to each other see [form definition](/form/definition). - -## Improvements that could be made - -- JSON schema definition could be expanded such that we can better automatically validate that a given config is valid and internally consistent - -- Generators could parse a given valid JSON form and generate the required database migrations to ensure all the expected fields exist and are of a compatible type - -- The parsed form could be visualised using something like GraphViz to help manually verify the coded config meets requirements diff --git a/docs/form/definition.md b/docs/form/definition.md index 3d27bb30e..8eb646284 100644 --- a/docs/form/definition.md +++ b/docs/form/definition.md @@ -6,26 +6,15 @@ nav_order: 3 # Form definition -The current system is built around a form definition written in JSON. At the top level every form will expect to have the following attributes: +The current system is built around a form definition constructed from various Form subclasses. At the top level every form will expect to have the following attributes: - Form type: this is to define whether the form is a lettings form or a sales form. The questions will differ between the types. - Start date: the start of the collection window for the form, this will usually be in April. -- End date: the end date of the collection window for the form, this will usually be in July, a year after the start date. +- Submission deadline: the official end date of the collection window for the form, this will usually be in July, a year after the start date. +- New logs end date: the end date for creating any new logs for this form +- Edit end date: the end date for editing any existing logs for this form - Sections: the sections in the form, this block is where the bulk of the form definition will be. -An example of this might look like the following: - -```json -{ - "form_type": "lettings", - "start_date": "2021-04-01T00:00:00.000+01:00", - "end_date": "2022-07-01T00:00:00.000+01:00", - "sections": { - ... - } -} -``` - Note that the end date of one form will overlap the start date of another to allow for late submissions. This means that every year there will be a period of time in which two forms are running simultaneously. A form is split up is as follows: @@ -39,24 +28,24 @@ Rails uses the model, view, controller (MVC) pattern which we follow. ## Form model -There is no need to manually initialise a form object as this is handled by the FormHandler class at boot time. If a new form needs to be added then a JSON file containing the form definition should be added to `config/forms` where the FormHandler will be able to locate it and instantiate it. +There is no need to manually initialise a form object as this is handled by the FormHandler class at boot time. A form has the following attributes: - `name`: The name of the form -- `setup_sections`: The setup section (this is not defined in the JSON, for more information see this) -- `form_definition`: The parsed form JSON -- `form_sections`: The sections found within the form definition JSON +- `setup_sections`: The setup section +- `form_sections`: The sections passed to form on init - `type`: The type of form (this is used to indicate if the form is for a sale or a letting) -- `sections`: The combination of the setup section with those found in the JSON definition +- `sections`: The combination of the setup section with form sections - `subsections`: The subsections of the form (these live under the sections) - `pages`: The pages of the form (these live under the subsections) - `questions`: The questions of the form (these live under the pages) - `start_date`: The start date of the form, in ISO 8601 format -- `end_date`: The end date of the form, in ISO 8601 format +- `submission_deadline`: The official end date of the form, in ISO 8601 format +- `new_logs_end_date`: The new logs end date of the form, in ISO 8601 format +- `edit_end_date`: The edit end date of the form, in ISO 8601 format -Each form has an `end_date` which for JSON forms is defined in the form definition JSON file and for code defined forms it is set to 1st July, 1 year after the start year. -Logs with a form that has `end_date` in the past can no longer be edited through the UI. +Logs with a form that has `edit_end_date` in the past can no longer be edited through the UI. ## Form views diff --git a/docs/form/index.md b/docs/form/index.md index 664b136c7..ed21e3b10 100644 --- a/docs/form/index.md +++ b/docs/form/index.md @@ -13,18 +13,12 @@ A paper form is produced for guidance and to help data providers collect the dat Data is accepted for a collection window for up to 3 months after it’s finished to allow for late data submission. This means that between April and July 2 versions of the form run simultaneously. -Other considerations that went into our design are being able to re-use as much of this solution for other data collections, and possibly having the ability to generate the form and/or form changes from a user interface. +Other initial considerations that went into our design are being able to re-use as much of this solution for other data collections, and possibly having the ability to generate the form and/or form changes from a user interface. -We haven’t used micro-services, preferring to deploy a single application but we have modelled the form itself as configuration in the form of a JSON structure that acts as a sort of DSL/form builder for the form. +Each form has historically been defined as a JSON configuration, but has since been replaced with subsection, page and question classes that contruct a form in code due to increased complexity. -The idea is to decouple the code that creates the required routes, controller methods, views etc to display the form from the actual wording of questions or order of pages such that it becomes possible to make changes to the form with little or no code changes. - -This should also mean that in the future it could be possible to create an interface that can construct the JSON config, which would open up the ability to make form changes to a wider audience. Doing this fully would require generating and running the necessary migrations for data storage, generating the required ActiveRecord methods to validate the data server side, and generating/updating API endpoints and documentation. All of this is likely to be beyond the scope of initial MVP but could be looked at in the future. - -Since initially the JSON config will not create database migrations or ActiveRecord model validations, it will instead assume that these have been correctly created for the config provided. The reasoning for this is the following assumptions: +To allow for easier content changes, the copy for questions has been extracted into translation files. The reasoning for this is the following assumptions: - The form will be tweaked regularly (amending questions wording, changing the order of questions or the page a question is displayed on) - The actual data collected will change very infrequently. Time series continuity is very important to ADD (Analysis and Data Directorate) so the actual data collected should stay largely consistent i.e. in general we can change the question wording in ways that makes the intent clearer or easier to understand, but not in ways that would make the data provider give a different answer. - -A form parser class will parse this config into ruby objects/methods that can be used as an API by the rest of the application, such that we could change the underlying config if needed (for example swap JSON for YAML or for DataBase objects) without needing to change the rest of the application. We’ll call this the Form Runner part of the application. diff --git a/docs/form/page.md b/docs/form/page.md index 47de24d98..7e0607a70 100644 --- a/docs/form/page.md +++ b/docs/form/page.md @@ -10,43 +10,44 @@ Pages sit below the [`Subsection`](subsection) level of a form definition. An example page might look something like this: -```json -"property_postcode": { - "header": "", - "description": "", - "questions": { - ... - }, - "depends_on": [ - { - "needstype": 1 +``` +class Form::Sales::Pages::PropertyPostcode < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = property_postcode + @depends_on = [{ "needstype" => 1 }] + @title_text = { + "translation": "translation1", + "arguments": [ + { + "key": "some_general_field", + "label": true, + "i18n_template": "template1" + } + ] } - ], - "title_text": { - "translation": "translation1", - "arguments": [ - { - "key": "some_general_field", - "label": true, - "i18n_template": "template1" - } - ] - }, - "informative_text": { - "translation": "translation2", - "arguments": [ - { - "key": "some_currency_method", - "label": false, - "i18n_template": "template2", - "currency": true, - } + @informative_text": { + "translation": "translation2", + "arguments": [ + { + "key": "some_currency_method", + "label": false, + "i18n_template": "template2", + "currency": true, + } + ] + } + end + + def questions + @questions ||= [ + Form::Sales::Questions::Question.new(nil, nil, self), ] - }, -} + end +end ``` -In the above example the the subsection has the id `property_postcode`. This id is used for the url of the web page, but the underscore is replaced with a hash, so the url for this page would be `[environment-url]/logs/[log-id]/property-postcode` e.g. on staging this url might look like the following: `https://dluhc-core-staging.london.cloudapps.digital/logs/1234/property-postcode`. +In the above example the the subsection has the id `property_postcode`. This id is used for the url of the web page, but the underscore is replaced with a dash, so the url for this page would be `[environment-url]/logs/[log-id]/property-postcode` e.g. on staging this url might look like the following: `https://staging.submit-social-housing-data.communities.gov.uk/logs/1234/property-postcode`. The header is optional but if provided is used for the heading displayed on the page. diff --git a/docs/form/question.md b/docs/form/question.md index dd3254e72..7112596cf 100644 --- a/docs/form/question.md +++ b/docs/form/question.md @@ -10,25 +10,25 @@ Questions are under the page level of the form definition. An example question might look something like this: -```json -"postcode_known": { - "check_answer_label": "Do you know the property postcode?", - "header": "Do you know the property’s postcode?", - "hint_text": "", - "type": "radio", - "answer_options": { - "1": { - "value": "Yes" +``` +class Form::Sales::Questions::PostcodeKnown < ::Form::Question + def initialize(id, hsh, page) + super + @id = postcode_known + @hint_text = "" + @header = "Do you know the property postcode?" + @check_answer_label = "Do you know the property postcode?" + @type = "radio" + @answer_options = { + "1" => { "value" => "Yes" }, + "0" => { "value" => "No" } }, - "0": { - "value": "No" - } - }, - "conditional_for": { - "postcode_full": [1] - }, - "hidden_in_check_answers": true -} + @conditional_for = { + "postcode_full" => [1] + }, + @hidden_in_check_answers = true + end +end ``` In the above example the the question has the id `postcode_known`. @@ -45,15 +45,11 @@ The `conditional_for` contains the value needed to be selected by the data input the `hidden_in_check_answers` is used to hide a value from displaying on the check answers page. You only need to provide this if you want to set it to true in order to hide the value for some reason e.g. it's one of two questions appearing on a page and the other question is displayed on the check answers page. It's also worth noting that you can declare this as a with a `depends_on` which can be useful for conditionally displaying values on the check answers page. For example: -```json -"hidden_in_check_answers": { - "depends_on": [ - { - "age6_known": 0 - }, - { - "age6_known": 1 - } +``` +@hidden_in_check_answers = { + "depends_on" => [ + { "age6_known" => 0 }, + { "age6_known" => 1 } ] } ``` @@ -62,25 +58,25 @@ Would mean the question the above is attached to would be hidden in the check an The answer the data inputter provides to some questions allows us to infer the values of other questions we might have asked in the form, allowing us to save the data inputters some time. An example of how this might look is as follows: -```json -"postcode_full": { - "check_answer_label": "Postcode", - "header": "What is the property’s postcode?", - "hint_text": "", - "type": "text", - "width": 5, - "inferred_answers": { - "la": { - "is_la_inferred": true +``` +class Form::Sales::Questions::PostcodeFull < ::Form::Question + def initialize(id, hsh, page) + super + @id = postcode_full + @hint_text = "" + @header = "What is the property’s postcode?"" + @check_answer_label = "Postcode"" + @type = "text" + @width = 5 + @inferred_answers = { + "la" => { "is_la_inferred" => true } } - }, - "inferred_check_answers_value": [{ - "condition": { - "postcode_known": 0 - }, - "value": "Not known" - }] -} + @inferred_check_answers_value => [{ + "condition" => { "postcode_known" => 0 }, + "value": "Not known" + }] + end +end ``` In the above example the width is an optional attribute and can be provided for text type questions to determine the width of the text box on the page when when the question is displayed to a user (this allows you to match the width of the text box on the page to that of the design for a question). diff --git a/docs/form/section.md b/docs/form/section.md index 514842355..e4443af3b 100644 --- a/docs/form/section.md +++ b/docs/form/section.md @@ -10,24 +10,22 @@ Sections sit at the top level of a form definition. An example section might look something like this: -```json -"sections": { - "tenancy_and_property": { - "label": "Property and tenancy information", - "subsections": { - "property_information": { - ... - }, - "tenancy_information": { - ... - } - } - }, - ... -} +``` +class Form::Sales::Sections::TenancyAndProperty < ::Form::Section + def initialize(id, hsh, form) + super + @id = "tenancy_and_property" + @label = "Property and tenancy information" + @description = "" + @subsections = [ + Form::Sales::Subsections::PropertyInformation.new(nil, nil, self), + Form::Sales::Subsections::TenancyInformation.new(nil, nil, self) + ] + end +end ``` -In the above example the section id would be `tenancy_and_property` and its subsections would be `property_information` and `tenancy_information`. +In the above example the section id would be `tenancy_and_property` and its subsections would be `PropertyInformation` and `TenancyInformation`. The label contains the text that users will see for that section in the task list page of a lettings log. diff --git a/docs/form/subsection.md b/docs/form/subsection.md index aa81c0259..5e659836a 100644 --- a/docs/form/subsection.md +++ b/docs/form/subsection.md @@ -10,29 +10,25 @@ Subsections sit below the [`Section`](section) level of a form definition. An example subsection might look something like this: -```json -"property_information": { - "label": "Property information", - "depends_on": [ - { - "setup": "completed" - } - ], - "pages": { - "property_postcode": { - ... - }, - "property_local_authority": { - ... - } - } -} +``` +class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection + def initialize(id, hsh, section) + super + @id = property_information + @depends_on = [{ "setup": "completed" }] + @label = "Property information" + end + + def pages + @pages ||= [Form::Sales::Pages::PropertyPostcode.new(nil, nil, self),Form::Sales::Pages::PropertyLocalAuthority.new(nil, nil, self)] + end +end ``` In the above example the the subsection has the id `property_information`. The `depends_on` contains the set of conditions that must be met for the section to be accessible to a data provider, in this example subsection depends on the completion of the setup section/subsection (note that this is a common condition as the answers provided to questions in the setup subsection often have an impact on what questions are asked of the data provider in later subsections of the form). The label contains the text that users will see for that subsection in the task list page of a lettings log. -The pages of the subsection in the example would be `property_postcode` and `property_local_authority`. +The pages of the subsection in the example would be `PropertyPostcode` and `PropertyLocalAuthority`. Subsections can contain one or more [pages](page). diff --git a/docs/setup.md b/docs/setup.md index d14fa58d9..4400a7ae2 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -79,15 +79,23 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage NodeJS version macOS (using nvm): ```bash - nvm install 16 - nvm use 16 + nvm install 20 + nvm use 20 + brew install yarn + ``` + + or you could run it without specifying the version and it should use the version from .nvmrc + + ```bash + nvm install + nvm use brew install yarn ``` Linux (Debian): ```bash - curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - + curl -sL https://deb.nodesource.com/setup_20.x | sudo bash - sudo apt -y install nodejs mkdir -p ~/.npm-packages npm config set prefix ~/.npm-packages 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/components/check_answers_summary_list_card_component_spec.rb b/spec/components/check_answers_summary_list_card_component_spec.rb index aa9ad1be5..c08fb3f42 100644 --- a/spec/components/check_answers_summary_list_card_component_spec.rb +++ b/spec/components/check_answers_summary_list_card_component_spec.rb @@ -6,7 +6,7 @@ RSpec.describe CheckAnswersSummaryListCardComponent, type: :component do let(:rendered) { render_inline(component) } let(:user) { create(:user) } - let(:log) { create(:lettings_log, :completed, age2: 99, retirement_value_check: 1) } + let(:log) { create(:lettings_log, :completed, sex1: "F", age2: 99, retirement_value_check: 1) } let(:subsection_id) { "household_characteristics" } let(:subsection) { log.form.get_subsection(subsection_id) } let(:questions) { subsection.applicable_questions(log) } diff --git a/spec/components/lettings_log_summary_component_spec.rb b/spec/components/lettings_log_summary_component_spec.rb index 52b6b1955..78ce5d804 100644 --- a/spec/components/lettings_log_summary_component_spec.rb +++ b/spec/components/lettings_log_summary_component_spec.rb @@ -3,9 +3,11 @@ require "rails_helper" RSpec.describe LettingsLogSummaryComponent, type: :component do let(:support_user) { FactoryBot.create(:user, :support) } let(:coordinator_user) { FactoryBot.create(:user) } + let(:organisation) { create(:organisation, name: "MHCLG") } + let(:log_user) { create(:user, name: "Danny Rojas", organisation:) } let(:propcode) { "P3647" } let(:tenancycode) { "T62863" } - let(:lettings_log) { FactoryBot.create(:lettings_log, needstype: 1, tenancycode:, propcode:, startdate: Time.zone.today) } + let(:lettings_log) { FactoryBot.create(:lettings_log, assigned_to: log_user, needstype: 1, tenancycode:, propcode:, startdate: Time.zone.today) } context "when rendering lettings log for a support user" do it "shows the log summary with organisational relationships" do diff --git a/spec/components/sales_log_summary_component_spec.rb b/spec/components/sales_log_summary_component_spec.rb index ce9b3a45c..ec5c3cd80 100644 --- a/spec/components/sales_log_summary_component_spec.rb +++ b/spec/components/sales_log_summary_component_spec.rb @@ -16,7 +16,7 @@ RSpec.describe SalesLogSummaryComponent, type: :component do it "shows the log summary with organisational relationships" do result = render_inline(described_class.new(current_user: support_user, log: sales_log)) - expect(result).to have_content("Owned by\n MHCLG") + expect(result).to have_content("Owned by\n #{sales_log.owning_organisation.name}") expect(result).not_to have_content("Managed by") end end diff --git a/spec/factories/collection_resource.rb b/spec/factories/collection_resource.rb index 0282e33e3..fb1895806 100644 --- a/spec/factories/collection_resource.rb +++ b/spec/factories/collection_resource.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :collection_resource, class: "CollectionResource" do resource_type { "paper_form" } - display_name { "lettings log for tenants (2021 to 2022)" } + display_name { "lettings paper form (2021 to 2022)" } short_display_name { "Paper Form" } year { 2024 } log_type { "lettings" } diff --git a/spec/factories/csv_download.rb b/spec/factories/csv_download.rb new file mode 100644 index 000000000..415f69de6 --- /dev/null +++ b/spec/factories/csv_download.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :csv_download do + download_type { "lettings" } + user { create(:user) } + organisation { user.organisation } + filename { "lettings.csv" } + expiration_time { 24.hours.to_i } + end +end diff --git a/spec/factories/lettings_log.rb b/spec/factories/lettings_log.rb index 8ebe44875..c8c51ecf3 100644 --- a/spec/factories/lettings_log.rb +++ b/spec/factories/lettings_log.rb @@ -56,8 +56,8 @@ FactoryBot.define do status { 2 } tenancycode { Faker::Name.initials(number: 10) } age1_known { 0 } - age1 { 35 } - sex1 { "F" } + age1 { Faker::Number.within(range: 25..45) } + sex1 { %w[F M X R].sample } ethnic_group { 0 } ethnic { 2 } national { 13 } @@ -67,8 +67,8 @@ FactoryBot.define do relat2 { "P" } age2_known { 0 } details_known_2 { 0 } - age2 { 32 } - sex2 { "M" } + age2 { Faker::Number.within(range: 25..45) } + sex2 { %w[F M X R].sample } ecstat2 { 6 } homeless { 1 } underoccupation_benefitcap { 0 } @@ -163,7 +163,7 @@ FactoryBot.define do uprn_known { 0 } joint { 3 } address_line1 { "Address line 1" } - town_or_city { "London" } + town_or_city { Faker::Address.city } ppcodenk { 1 } tshortfall_known { 1 } after(:build) do |log, _evaluator| diff --git a/spec/factories/merge_request.rb b/spec/factories/merge_request.rb index 19020fce1..4b33e4002 100644 --- a/spec/factories/merge_request.rb +++ b/spec/factories/merge_request.rb @@ -2,6 +2,7 @@ FactoryBot.define do factory :merge_request do status { "incomplete" } merge_date { nil } + has_helpdesk_ticket { true } helpdesk_ticket { "MSD-99999" } association :requesting_organisation, factory: :organisation end diff --git a/spec/factories/organisation.rb b/spec/factories/organisation.rb index 6623ff355..ecab5bf81 100644 --- a/spec/factories/organisation.rb +++ b/spec/factories/organisation.rb @@ -1,11 +1,11 @@ FactoryBot.define do factory :organisation do - name { "MHCLG" } - address_line1 { "2 Marsham Street" } - address_line2 { "London" } + name { Faker::Company.name } + address_line1 { Faker::Address.street_address } + address_line2 { Faker::Address.city } provider_type { "LA" } - housing_registration_no { "1234" } - postcode { "SW1P 4DF" } + housing_registration_no { rand(99_999).to_s } + postcode { Faker::Address.postcode } created_at { Time.zone.now } updated_at { Time.zone.now } holds_own_stock { true } @@ -41,7 +41,13 @@ FactoryBot.define do trait :prp do provider_type { "PRP" } end + trait :la do + provider_type { "LA" } + end + trait :holds_own_stock do + holds_own_stock { true } + end trait :does_not_own_stock do holds_own_stock { false } end @@ -53,6 +59,10 @@ FactoryBot.define do data_protection_confirmation { nil } end + + trait :if_unique do + initialize_with { Organisation.find_or_create_by(name:) } + end end factory :organisation_rent_period do diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb index b809b9daf..687b042a4 100644 --- a/spec/factories/sales_log.rb +++ b/spec/factories/sales_log.rb @@ -5,7 +5,7 @@ FactoryBot.define do created_by { assigned_to } owning_organisation { assigned_to.organisation } - managing_organisation { owning_organisation } + managing_organisation { assigned_to.organisation } created_at { Time.zone.now } updated_at { Time.zone.now } trait :in_progress do @@ -32,6 +32,17 @@ FactoryBot.define do jointpur { 2 } noint { 2 } privacynotice { 1 } + purchid { rand(999_999_999).to_s } + end + trait :discounted_ownership_setup_complete do + saledate_today + ownershipsch { 2 } + type { 9 } + jointpur { 1 } + jointmore { 1 } + noint { 2 } + privacynotice { 1 } + purchid { rand(999_999_999).to_s } end trait :outright_sale_setup_complete do saledate_today @@ -67,14 +78,14 @@ FactoryBot.define do noint { 2 } privacynotice { 1 } age1_known { 0 } - age1 { 30 } - sex1 { "X" } + age1 { Faker::Number.within(range: 27..45) } + sex1 { %w[F M X R].sample } national { 18 } buy1livein { 1 } relat2 { "P" } proptype { 1 } age2_known { 0 } - age2 { 35 } + age2 { Faker::Number.within(range: 25..45) } builtype { 1 } ethnic { 3 } ethnic_group { 17 } @@ -104,16 +115,16 @@ FactoryBot.define do inc2mort { 1 } uprn_known { 0 } address_line1 { "Address line 1" } - town_or_city { "Town or city" } + town_or_city { Faker::Address.city } la_known { 1 } la { "E09000003" } savingsnk { 1 } prevown { 1 } prevshared { 2 } - sex3 { "X" } - sex4 { "X" } - sex5 { "X" } - sex6 { "X" } + sex3 { %w[F M X R].sample } + sex4 { %w[F M X R].sample } + sex5 { %w[F M X R].sample } + sex6 { %w[F M X R].sample } mortgage { 20_000 } ecstat3 { 9 } ecstat4 { 3 } @@ -122,6 +133,7 @@ FactoryBot.define do disabled { 1 } deposit { 80_000 } value { 110_000 } + value_value_check { 0 } grant { 10_000 } proplen { 10 } pregyrha { 1 } @@ -161,6 +173,13 @@ FactoryBot.define do log.uprn = "10033558653" log.uprn_selection = 1 end + if log.saledate >= Time.zone.local(2025, 4, 1) + log.relat2 = "X" if log.relat2 == "C" + log.relat3 = "X" if log.relat3 == "C" + log.relat4 = "X" if log.relat4 == "C" + log.relat5 = "X" if log.relat5 == "C" + log.relat6 = "X" if log.relat6 == "C" + end end end trait :with_uprn do @@ -174,5 +193,12 @@ FactoryBot.define do trait :imported do old_id { Random.hex } end + trait :ignore_validation_errors do + to_create do |instance| + instance.valid? + instance.errors.clear + instance.save!(validate: false) + end + end end end diff --git a/spec/factories/scheme.rb b/spec/factories/scheme.rb index 77d74b70a..e7ecc8b60 100644 --- a/spec/factories/scheme.rb +++ b/spec/factories/scheme.rb @@ -41,5 +41,13 @@ FactoryBot.define do support_type { 2 } intended_stay { "M" } end + 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/factories/user.rb b/spec/factories/user.rb index 956d88332..8708e319b 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :user do sequence(:email) { "test#{SecureRandom.hex}@example.com" } - name { "Danny Rojas" } + name { Faker::Name.name } password { "pAssword1" } organisation { association :organisation, with_dsa: is_dpo ? false : true } role { "data_provider" } @@ -40,5 +40,9 @@ FactoryBot.define do ) end end + + trait :if_unique do + initialize_with { User.find_or_create_by(email:) } + end end end diff --git a/spec/features/accessibility_spec.rb b/spec/features/accessibility_spec.rb index 15cd87ebb..889fcdf26 100644 --- a/spec/features/accessibility_spec.rb +++ b/spec/features/accessibility_spec.rb @@ -138,15 +138,18 @@ RSpec.describe "Accessibility", js: true do routes = find_routes("sales-log", sales_log, bulk_upload) - routes.reject { |path| + routes = routes.reject { |path| path.include?("/edit") || path.include?("/new") || path.include?("*page") || path.include?("/sales-logs/bulk-upload-logs/#{bulk_upload.id}") || path.include?("bulk-upload-soft-validations-check") || path.include?("filters/update") || path == "/sales-logs/bulk-upload-resume/#{bulk_upload.id}" || path == "/sales-logs/bulk-upload-logs" || + path.include?("/check-answers") || other_form_page_ids.any? { |page_id| path.include?(page_id.dasherize) } || sales_log_pages.any? { |page| path.include?(page.id.dasherize) && !page.routed_to?(sales_log, user) } }.uniq + + routes + sales_log.form.subsections.map(&:id).map { |id| "/sales-logs/#{sales_log.id}/#{id.dasherize}/check-answers" } end before do diff --git a/spec/features/lettings_log_spec.rb b/spec/features/lettings_log_spec.rb index 092ab2b40..efb7e7665 100644 --- a/spec/features/lettings_log_spec.rb +++ b/spec/features/lettings_log_spec.rb @@ -276,7 +276,7 @@ RSpec.describe "Lettings Log Features" do expect(breadcrumbs.length).to eq 3 expect(breadcrumbs[0].text).to eq "Home" expect(breadcrumbs[0][:href]).to eq root_path - expect(breadcrumbs[1].text).to eq "Lettings logs (MHCLG)" + expect(breadcrumbs[1].text).to eq "Lettings logs (#{lettings_log.owning_organisation.name})" expect(breadcrumbs[1][:href]).to eq lettings_logs_organisation_path(lettings_log.owning_organisation) expect(breadcrumbs[2].text).to eq "Log #{lettings_log.id}" expect(breadcrumbs[2][:href]).to eq lettings_log_path(lettings_log) @@ -292,7 +292,7 @@ RSpec.describe "Lettings Log Features" do expect(breadcrumbs.length).to eq 3 expect(breadcrumbs[0].text).to eq "Home" expect(breadcrumbs[0][:href]).to eq root_path - expect(breadcrumbs[1].text).to eq "Lettings logs (MHCLG)" + expect(breadcrumbs[1].text).to eq "Lettings logs (#{lettings_log.owning_organisation.name})" expect(breadcrumbs[1][:href]).to eq lettings_logs_organisation_path(lettings_log.owning_organisation) expect(breadcrumbs[2].text).to eq "Log #{lettings_log.id}" expect(breadcrumbs[2][:href]).to eq lettings_log_path(lettings_log) diff --git a/spec/features/sales_log_spec.rb b/spec/features/sales_log_spec.rb index ceff00f94..879f2b5c8 100644 --- a/spec/features/sales_log_spec.rb +++ b/spec/features/sales_log_spec.rb @@ -259,7 +259,7 @@ RSpec.describe "Sales Log Features" do expect(breadcrumbs.length).to eq 3 expect(breadcrumbs[0].text).to eq "Home" expect(breadcrumbs[0][:href]).to eq root_path - expect(breadcrumbs[1].text).to eq "Sales logs (MHCLG)" + expect(breadcrumbs[1].text).to eq "Sales logs (#{sales_log.owning_organisation.name})" expect(breadcrumbs[1][:href]).to eq sales_logs_organisation_path(sales_log.owning_organisation) expect(breadcrumbs[2].text).to eq "Log #{sales_log.id}" expect(breadcrumbs[2][:href]).to eq sales_log_path(sales_log.id) @@ -273,7 +273,7 @@ RSpec.describe "Sales Log Features" do expect(breadcrumbs.length).to eq 3 expect(breadcrumbs[0].text).to eq "Home" expect(breadcrumbs[0][:href]).to eq root_path - expect(breadcrumbs[1].text).to eq "Sales logs (MHCLG)" + expect(breadcrumbs[1].text).to eq "Sales logs (#{sales_log.owning_organisation.name})" expect(breadcrumbs[1][:href]).to eq sales_logs_organisation_path(sales_log.owning_organisation) expect(breadcrumbs[2].text).to eq "Log #{sales_log.id}" expect(breadcrumbs[2][:href]).to eq sales_log_path(sales_log.id) diff --git a/spec/features/schemes_spec.rb b/spec/features/schemes_spec.rb index 33ab00b34..dfb56665b 100644 --- a/spec/features/schemes_spec.rb +++ b/spec/features/schemes_spec.rb @@ -5,9 +5,10 @@ RSpec.describe "Schemes scheme Features" do include SchemesHelpers context "when viewing list of schemes" do context "when I am signed as a coordinator user and there are schemes in the database" do - let!(:user) { FactoryBot.create(:user, :data_coordinator, last_sign_in_at: Time.zone.now) } - let!(:schemes) { FactoryBot.create_list(:scheme, 5, owning_organisation: user.organisation) } - let!(:scheme_to_search) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } + let(:organisation) { FactoryBot.create(:organisation, name: "MHCLG") } + let!(:user) { FactoryBot.create(:user, :data_coordinator, organisation:, last_sign_in_at: Time.zone.now) } + let!(:schemes) { FactoryBot.create_list(:scheme, 5, owning_organisation: organisation) } + let!(:scheme_to_search) { FactoryBot.create(:scheme, owning_organisation: organisation) } before do Timecop.freeze(Time.zone.local(2024, 3, 1)) @@ -225,7 +226,7 @@ RSpec.describe "Schemes scheme Features" do end it "shows list of links to the organisation's schemes" do - click_on("Schemes (MHCLG)") + click_on("Schemes (#{user.organisation.name})") same_organisation_schemes.each do |scheme| expect(page).to have_link(scheme.service_name) expect(page).to have_content(scheme.id_to_display) @@ -677,7 +678,7 @@ RSpec.describe "Schemes scheme Features" do end it "adds scheme to the list of schemes" do - expect(page).to have_content "#{scheme.service_name} has been created. It does not require helpdesk approval." + expect(page).to have_content "#{scheme.service_name} has been created." click_link "Schemes" expect(page).to have_content "Supported housing schemes" expect(page).to have_content scheme.id_to_display 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/collection_resources_helper_spec.rb b/spec/helpers/collection_resources_helper_spec.rb index 9fc77a4c9..05c164fc1 100644 --- a/spec/helpers/collection_resources_helper_spec.rb +++ b/spec/helpers/collection_resources_helper_spec.rb @@ -94,9 +94,9 @@ RSpec.describe CollectionResourcesHelper do context "and next year resources were manually released" do before do - create(:collection_resource, year: 2025, resource_type: "paper_form", display_name: "lettings log for tenants (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "paper_form", display_name: "lettings paper form (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) create(:collection_resource, year: 2025, resource_type: "bulk_upload_template", display_name: "bulk upload template (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) - create(:collection_resource, year: 2025, resource_type: "bulk_upload_specification", display_name: "sales log for tenants (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "bulk_upload_specification", display_name: "sales paper form (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) end it "reutrns current and next years" do @@ -121,7 +121,7 @@ RSpec.describe CollectionResourcesHelper do describe "#document_list_component_items" do let(:resources) do [ - build(:collection_resource, year: 2023, resource_type: "paper_form", display_name: "lettings log for tenants (2023 to 2024)", download_filename: "2023_24_lettings_paper_form.pdf"), + build(:collection_resource, year: 2023, resource_type: "paper_form", display_name: "lettings paper form (2023 to 2024)", download_filename: "2023_24_lettings_paper_form.pdf"), build(:collection_resource, year: 2023, resource_type: "bulk_upload_template", display_name: "bulk upload template (2023 to 2024)", download_filename: "2023_24_lettings_bulk_upload_template.xlsx"), ] end @@ -134,7 +134,7 @@ RSpec.describe CollectionResourcesHelper do it "returns component items" do expect(document_list_component_items(resources)).to eq([ { - name: "Download the lettings log for tenants (2023 to 2024)", + name: "Download the lettings paper form (2023 to 2024)", href: "/collection-resources/lettings/2023/paper_form/download", metadata: "PDF, 286 KB", }, @@ -150,7 +150,7 @@ RSpec.describe CollectionResourcesHelper do describe "#document_list_edit_component_items" do let(:resources) do [ - build(:collection_resource, year: 2023, resource_type: "paper_form", display_name: "lettings log for tenants (2023 to 2024)", download_filename: "2023_24_lettings_paper_form.pdf"), + build(:collection_resource, year: 2023, resource_type: "paper_form", display_name: "lettings paper form (2023 to 2024)", download_filename: "2023_24_lettings_paper_form.pdf"), build(:collection_resource, year: 2023, resource_type: "bulk_upload_template", display_name: "bulk upload template (2023 to 2024)", download_filename: "2023_24_lettings_bulk_upload_template.xlsx"), ] end @@ -199,9 +199,9 @@ RSpec.describe CollectionResourcesHelper do context "and the resources have been manually released" do before do - create(:collection_resource, year: 2025, resource_type: "paper_form", display_name: "lettings log for tenants (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "paper_form", display_name: "lettings paper form (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) create(:collection_resource, year: 2025, resource_type: "bulk_upload_template", display_name: "bulk upload template (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) - create(:collection_resource, year: 2025, resource_type: "bulk_upload_specification", display_name: "sales log for tenants (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "bulk_upload_specification", display_name: "sales paper form (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) end it "returns false" do diff --git a/spec/helpers/organisations_helper_spec.rb b/spec/helpers/organisations_helper_spec.rb index 9ebe12d1f..ea0d9b158 100644 --- a/spec/helpers/organisations_helper_spec.rb +++ b/spec/helpers/organisations_helper_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe OrganisationsHelper do include TagHelper describe "display_organisation_attributes" do - let(:organisation) { create(:organisation) } + let(:organisation) { create(:organisation, :la, :holds_own_stock, address_line1: "2 Marsham Street", address_line2: "London", postcode: "SW1P 4DF", housing_registration_no: 1234, organisation_rent_periods: []) } it "has the correct values" do expect(display_organisation_attributes(organisation)).to eq( diff --git a/spec/helpers/tab_nav_helper_spec.rb b/spec/helpers/tab_nav_helper_spec.rb index 4a2d13377..89f867775 100644 --- a/spec/helpers/tab_nav_helper_spec.rb +++ b/spec/helpers/tab_nav_helper_spec.rb @@ -15,7 +15,7 @@ RSpec.describe TabNavHelper do describe "#org_cell" do it "returns the users org name and role separated by a newline character" do - expected_html = "MHCLG\nData provider" + expected_html = "#{organisation.name}\nData provider" expect(org_cell(current_user)).to match(expected_html) end 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/jobs/email_csv_job_spec.rb b/spec/jobs/email_csv_job_spec.rb index 8e8a027ea..59cf7bf7d 100644 --- a/spec/jobs/email_csv_job_spec.rb +++ b/spec/jobs/email_csv_job_spec.rb @@ -3,8 +3,6 @@ require "rails_helper" describe EmailCsvJob do include Helpers - test_url = :test_url - let(:job) { described_class.new } let(:user) { FactoryBot.create(:user) } let(:storage_service) { instance_double(Storage::S3Service) } @@ -22,7 +20,6 @@ describe EmailCsvJob do before do allow(Storage::S3Service).to receive(:new).and_return(storage_service) allow(storage_service).to receive(:write_file) - allow(storage_service).to receive(:get_presigned_url).and_return(test_url) allow(Csv::SalesLogCsvService).to receive(:new).and_return(sales_log_csv_service) allow(sales_log_csv_service).to receive(:prepare_csv).and_return("") @@ -67,6 +64,16 @@ describe EmailCsvJob do expect(lettings_log_csv_service).to receive(:prepare_csv).with(lettings_logs) job.perform(user, nil, {}, nil, nil, codes_only_export) end + + it "creates a CsvDownload record" do + job.perform(user, nil, {}, nil, nil, codes_only_export, "lettings") + expect(CsvDownload.count).to eq(1) + expect(CsvDownload.first.user).to eq(user) + expect(CsvDownload.first.organisation).to eq(user.organisation) + expect(CsvDownload.first.filename).to match(/lettings-logs-.*\.csv/) + expect(CsvDownload.first.download_type).to eq("lettings") + expect(CsvDownload.first.expiration_time).to eq(172_800) + end end context "when exporting sales logs" do @@ -102,10 +109,20 @@ describe EmailCsvJob do expect(sales_log_csv_service).to receive(:prepare_csv).with(sales_logs) job.perform(user, nil, {}, nil, nil, codes_only_export, "sales") end + + it "creates a CsvDownload record" do + job.perform(user, nil, {}, nil, nil, codes_only_export, "sales") + expect(CsvDownload.count).to eq(1) + expect(CsvDownload.first.user).to eq(user) + expect(CsvDownload.first.organisation).to eq(user.organisation) + expect(CsvDownload.first.filename).to match(/sales-logs-.*\.csv/) + expect(CsvDownload.first.download_type).to eq("sales") + expect(CsvDownload.first.expiration_time).to eq(172_800) + end end it "sends an E-mail with the presigned URL and duration" do - expect(mailer).to receive(:send_csv_download_mail).with(user, test_url, instance_of(Integer)) + expect(mailer).to receive(:send_csv_download_mail).with(user, /csv-downloads/, instance_of(Integer)) job.perform(user) end end diff --git a/spec/jobs/scheme_email_csv_job_spec.rb b/spec/jobs/scheme_email_csv_job_spec.rb index 5ddaa91a6..efb75b698 100644 --- a/spec/jobs/scheme_email_csv_job_spec.rb +++ b/spec/jobs/scheme_email_csv_job_spec.rb @@ -3,10 +3,8 @@ require "rails_helper" describe SchemeEmailCsvJob do include Helpers - test_url = :test_url - let(:job) { described_class.new } - let(:storage_service) { instance_double(Storage::S3Service, write_file: nil, get_presigned_url: test_url) } + let(:storage_service) { instance_double(Storage::S3Service, write_file: nil) } let(:mailer) { instance_double(CsvDownloadMailer, send_csv_download_mail: nil) } let(:user) { FactoryBot.create(:user) } @@ -53,6 +51,16 @@ describe SchemeEmailCsvJob do job.perform(user, nil, {}, nil, nil, download_type) end end + + it "creates a CsvDownload record" do + job.perform(user, nil, {}, nil, nil, download_type) + expect(CsvDownload.count).to eq(1) + expect(CsvDownload.first.user).to eq(user) + expect(CsvDownload.first.organisation).to eq(user.organisation) + expect(CsvDownload.first.filename).to match(/schemes-.*\.csv/) + expect(CsvDownload.first.download_type).to eq("schemes") + expect(CsvDownload.first.expiration_time).to eq(172_800) + end end context "when download type locations" do @@ -62,6 +70,16 @@ describe SchemeEmailCsvJob do expect(storage_service).to receive(:write_file).with(/locations-.*\.csv/, anything) job.perform(user, nil, {}, nil, nil, download_type) end + + it "creates a CsvDownload record" do + job.perform(user, nil, {}, nil, nil, download_type) + expect(CsvDownload.count).to eq(1) + expect(CsvDownload.first.user).to eq(user) + expect(CsvDownload.first.organisation).to eq(user.organisation) + expect(CsvDownload.first.filename).to match(/locations-.*\.csv/) + expect(CsvDownload.first.download_type).to eq("locations") + expect(CsvDownload.first.expiration_time).to eq(172_800) + end end context "when download type combined" do @@ -71,6 +89,16 @@ describe SchemeEmailCsvJob do expect(storage_service).to receive(:write_file).with(/schemes-and-locations.*\.csv/, anything) job.perform(user, nil, {}, nil, nil, download_type) end + + it "creates a CsvDownload record" do + job.perform(user, nil, {}, nil, nil, download_type) + expect(CsvDownload.count).to eq(1) + expect(CsvDownload.first.user).to eq(user) + expect(CsvDownload.first.organisation).to eq(user.organisation) + expect(CsvDownload.first.filename).to match(/schemes-and-locations-.*\.csv/) + expect(CsvDownload.first.download_type).to eq("combined") + expect(CsvDownload.first.expiration_time).to eq(172_800) + end end it "includes the organisation name in the filename when one is provided" do @@ -117,7 +145,7 @@ describe SchemeEmailCsvJob do end it "sends an E-mail with the presigned URL and duration" do - expect(mailer).to receive(:send_csv_download_mail).with(user, test_url, instance_of(Integer)) + expect(mailer).to receive(:send_csv_download_mail).with(user, /csv-downloads/, instance_of(Integer)) job.perform(user) end end 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/lettings/questions/homeless_spec.rb b/spec/models/form/lettings/questions/homeless_spec.rb index f18a8ece7..7f2c1b054 100644 --- a/spec/models/form/lettings/questions/homeless_spec.rb +++ b/spec/models/form/lettings/questions/homeless_spec.rb @@ -28,10 +28,6 @@ RSpec.describe Form::Lettings::Questions::Homeless, type: :model do }) end - it "has no hint text" do - expect(question.hint_text).to be_empty - end - it "has the correct check_answers_card_number" do expect(question.check_answers_card_number).to eq(0) end diff --git a/spec/models/form/page_spec.rb b/spec/models/form/page_spec.rb index 9315fb237..3ec929fc4 100644 --- a/spec/models/form/page_spec.rb +++ b/spec/models/form/page_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Form::Page, type: :model do let(:enabled) { true } let(:depends_on_met) { true } let(:form) { instance_double(Form, depends_on_met:, type: "form-type", start_date: Time.utc(2024, 12, 25)) } - let(:subsection) { instance_double(Form::Subsection, depends_on:, enabled?: enabled, form:, id: "subsection-id") } + let(:subsection) { instance_double(Form::Subsection, depends_on:, enabled?: enabled, form:, id: "subsection-id", copy_key: "subsection-copy-key") } let(:page_id) { "net_income" } let(:questions) { [["earnings", { "conditional_for" => { "age1": nil }, "type" => "radio" }], %w[incfreq]] } let(:page_definition) do @@ -25,7 +25,7 @@ RSpec.describe Form::Page, type: :model do end it "sets copy_key in the default style" do - expect(page.copy_key).to eq("#{form.type}.#{subsection.id}.#{questions[0][0]}") + expect(page.copy_key).to eq("#{form.type}.#{subsection.copy_key}.#{questions[0][0]}") end context "when header is not provided" do diff --git a/spec/models/form/question_spec.rb b/spec/models/form/question_spec.rb index 651d167bd..8b9ab7a85 100644 --- a/spec/models/form/question_spec.rb +++ b/spec/models/form/question_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Form::Question, type: :model do let(:inferred_check_answers_value) { [{ "condition" => { "postcode_known" => 0 }, "value" => "Weekly" }] } let(:form) { instance_double(Form, depends_on_met:, conditional_question_conditions:, type: "form-type", start_date: Time.utc(2024, 12, 25)) } - let(:subsection) { instance_double(Form::Subsection, form:, id: "subsection-id") } + let(:subsection) { instance_double(Form::Subsection, form:, id: "subsection-id", copy_key: "subsection-copy-key") } let(:page) { instance_double(Form::Page, subsection:, routed_to?: true, questions: form_questions) } let(:question_id) { "earnings" } let(:question_definition) do @@ -39,7 +39,7 @@ RSpec.describe Form::Question, type: :model do end it "sets copy_key in the default style" do - expect(question.copy_key).to eq("#{form.type}.#{subsection.id}.#{question_id}") + expect(question.copy_key).to eq("#{form.type}.#{subsection.copy_key}.#{question_id}") end context "when copy is not provided" do 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/buyer1_income_max_value_check_spec.rb b/spec/models/form/sales/pages/buyer1_income_discounted_max_value_check_spec.rb similarity index 84% rename from spec/models/form/sales/pages/buyer1_income_max_value_check_spec.rb rename to spec/models/form/sales/pages/buyer1_income_discounted_max_value_check_spec.rb index fa5cf1e7c..2282a9535 100644 --- a/spec/models/form/sales/pages/buyer1_income_max_value_check_spec.rb +++ b/spec/models/form/sales/pages/buyer1_income_discounted_max_value_check_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Form::Sales::Pages::Buyer1IncomeMaxValueCheck, type: :model do +RSpec.describe Form::Sales::Pages::Buyer1IncomeDiscountedMaxValueCheck, type: :model do subject(:page) { described_class.new(page_id, page_definition, subsection, check_answers_card_number: 1) } let(:page_id) { "prefix_buyer_1_income_max_value_check" } @@ -23,7 +23,7 @@ RSpec.describe Form::Sales::Pages::Buyer1IncomeMaxValueCheck, type: :model do it "has correct depends_on" do expect(page.depends_on).to eq([ { - "income1_over_soft_max?" => true, + "income1_over_soft_max_for_discounted_ownership?" => true, }, ]) end diff --git a/spec/models/form/sales/pages/buyer1_income_min_value_check_spec.rb b/spec/models/form/sales/pages/buyer1_income_ecstat_value_check_spec.rb similarity index 75% rename from spec/models/form/sales/pages/buyer1_income_min_value_check_spec.rb rename to spec/models/form/sales/pages/buyer1_income_ecstat_value_check_spec.rb index 79d61ce06..9d710aebb 100644 --- a/spec/models/form/sales/pages/buyer1_income_min_value_check_spec.rb +++ b/spec/models/form/sales/pages/buyer1_income_ecstat_value_check_spec.rb @@ -1,9 +1,9 @@ require "rails_helper" -RSpec.describe Form::Sales::Pages::Buyer1IncomeMinValueCheck, type: :model do +RSpec.describe Form::Sales::Pages::Buyer1IncomeEcstatValueCheck, type: :model do subject(:page) { described_class.new(page_id, page_definition, subsection) } - let(:page_id) { "prefix_buyer_1_income_min_value_check" } + let(:page_id) { "prefix_buyer_1_income_ecstat_value_check" } let(:page_definition) { nil } let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1)) } let(:subsection) { instance_double(Form::Subsection, form:) } @@ -17,13 +17,13 @@ RSpec.describe Form::Sales::Pages::Buyer1IncomeMinValueCheck, type: :model do end it "has the correct id" do - expect(page.id).to eq("prefix_buyer_1_income_min_value_check") + expect(page.id).to eq("prefix_buyer_1_income_ecstat_value_check") end it "has correct depends_on" do expect(page.depends_on).to eq([ { - "income1_under_soft_min?" => true, + "income1_outside_soft_range_for_ecstat?" => true, }, ]) end diff --git a/spec/models/form/sales/pages/buyer2_income_max_value_check_spec.rb b/spec/models/form/sales/pages/buyer2_income_discounted_max_value_check_spec.rb similarity index 84% rename from spec/models/form/sales/pages/buyer2_income_max_value_check_spec.rb rename to spec/models/form/sales/pages/buyer2_income_discounted_max_value_check_spec.rb index f467db18d..36a04cedb 100644 --- a/spec/models/form/sales/pages/buyer2_income_max_value_check_spec.rb +++ b/spec/models/form/sales/pages/buyer2_income_discounted_max_value_check_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Form::Sales::Pages::Buyer2IncomeMaxValueCheck, type: :model do +RSpec.describe Form::Sales::Pages::Buyer2IncomeDiscountedMaxValueCheck, type: :model do subject(:page) { described_class.new(page_id, page_definition, subsection, check_answers_card_number: 2) } let(:page_id) { "prefix_buyer_2_income_max_value_check" } @@ -23,7 +23,7 @@ RSpec.describe Form::Sales::Pages::Buyer2IncomeMaxValueCheck, type: :model do it "has correct depends_on" do expect(page.depends_on).to eq([ { - "income2_over_soft_max?" => true, + "income2_over_soft_max_for_discounted_ownership?" => true, }, ]) end diff --git a/spec/models/form/sales/pages/buyer2_income_min_value_check_spec.rb b/spec/models/form/sales/pages/buyer2_income_ecstat_value_check_spec.rb similarity index 72% rename from spec/models/form/sales/pages/buyer2_income_min_value_check_spec.rb rename to spec/models/form/sales/pages/buyer2_income_ecstat_value_check_spec.rb index 44b85ef9e..2ced58f17 100644 --- a/spec/models/form/sales/pages/buyer2_income_min_value_check_spec.rb +++ b/spec/models/form/sales/pages/buyer2_income_ecstat_value_check_spec.rb @@ -1,9 +1,9 @@ require "rails_helper" -RSpec.describe Form::Sales::Pages::Buyer2IncomeMinValueCheck, type: :model do +RSpec.describe Form::Sales::Pages::Buyer2IncomeEcstatValueCheck, type: :model do subject(:page) { described_class.new(page_id, page_definition, subsection) } - let(:page_id) { "prefix_buyer_2_income_min_value_check" } + let(:page_id) { "prefix_buyer_2_income_ecstat_value_check" } let(:page_definition) { nil } let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1)) } let(:subsection) { instance_double(Form::Subsection, form:) } @@ -17,13 +17,13 @@ RSpec.describe Form::Sales::Pages::Buyer2IncomeMinValueCheck, type: :model do end it "has the correct id" do - expect(page.id).to eq("prefix_buyer_2_income_min_value_check") + expect(page.id).to eq("prefix_buyer_2_income_ecstat_value_check") end it "has correct depends_on" do expect(page.depends_on).to eq([ { - "income2_under_soft_min?" => true, + "income2_outside_soft_range_for_ecstat?" => true, }, ]) end diff --git a/spec/models/form/sales/pages/buyer_interview_spec.rb b/spec/models/form/sales/pages/buyer_interview_spec.rb index f7c6bbb70..ea707a0fe 100644 --- a/spec/models/form/sales/pages/buyer_interview_spec.rb +++ b/spec/models/form/sales/pages/buyer_interview_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Form::Sales::Pages::BuyerInterview, type: :model do let(:page_id) { "buyer_interview" } let(:page_definition) { nil } let(:form) { instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2024_or_later?: false) } - let(:subsection) { instance_double(Form::Subsection, form:, id: "setup") } + let(:subsection) { instance_double(Form::Subsection, form:, id: "setup", copy_key: "subsection_copy_key") } it "has correct subsection" do expect(page.subsection).to eq(subsection) @@ -24,43 +24,23 @@ RSpec.describe Form::Sales::Pages::BuyerInterview, type: :model do expect(page.description).to be_nil end - context "when form is before 2024" do - let(:subsection) { instance_double(Form::Subsection, form:, id: "household_characteristics") } + context "when there are joint buyers" do + subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: true) } - context "when there are joint buyers" do - subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: true) } + let(:subsection) { instance_double(Form::Subsection, form:, copy_key: "subsection_copy_key") } - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.household_characteristics.noint.joint_purchase") - end - end - - context "when there is a single buyer" do - subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: false) } - - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.household_characteristics.noint.not_joint_purchase") - end + it "has the expected copy_key" do + expect(page.copy_key).to eq("sales.subsection_copy_key.noint.joint_purchase") end end - context "when form is after 2024" do - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2024_or_later?: true) } - - context "when there are joint buyers" do - subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: true) } - - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.setup.noint.joint_purchase") - end - end + context "when there is a single buyer" do + subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: false) } - context "when there is a single buyer" do - subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: false) } + let(:subsection) { instance_double(Form::Subsection, form:, copy_key: "subsection_copy_key") } - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.setup.noint.not_joint_purchase") - end + it "has the expected copy_key" do + expect(page.copy_key).to eq("sales.subsection_copy_key.noint.not_joint_purchase") end end end diff --git a/spec/models/form/sales/pages/combined_income_max_value_check_spec.rb b/spec/models/form/sales/pages/combined_income_max_value_check_spec.rb index f9b9954d9..85dd29c5c 100644 --- a/spec/models/form/sales/pages/combined_income_max_value_check_spec.rb +++ b/spec/models/form/sales/pages/combined_income_max_value_check_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Form::Sales::Pages::CombinedIncomeMaxValueCheck, type: :model do it "has correct depends_on" do expect(page.depends_on).to eq([ { - "combined_income_over_soft_max?" => true, + "combined_income_over_soft_max_for_discounted_ownership?" => true, }, ]) end 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/living_before_purchase_spec.rb b/spec/models/form/sales/pages/living_before_purchase_spec.rb index 26026471b..b597f90e9 100644 --- a/spec/models/form/sales/pages/living_before_purchase_spec.rb +++ b/spec/models/form/sales/pages/living_before_purchase_spec.rb @@ -5,17 +5,19 @@ RSpec.describe Form::Sales::Pages::LivingBeforePurchase, type: :model do let(:page_id) { nil } let(:page_definition) { nil } - let(:subsection) { instance_double(Form::Subsection) } + let(:start_year) { 2022 } + let(:form) { Form.new(nil, start_year, [], "sales") } + let(:subsection) { instance_double(Form::Subsection, depends_on: nil, form:) } it "has correct subsection" do expect(page.subsection).to eq(subsection) end describe "questions" do - let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) } + let(:subsection) { instance_double(Form::Subsection, form:, depends_on: nil) } context "when 2022" do - let(:start_date) { Time.utc(2022, 2, 8) } + let(:start_year) { 2022 } it "has correct questions" do expect(page.questions.map(&:id)).to eq(%w[proplen]) @@ -23,7 +25,7 @@ RSpec.describe Form::Sales::Pages::LivingBeforePurchase, type: :model do end context "when 2023" do - let(:start_date) { Time.utc(2023, 2, 8) } + let(:start_year) { 2023 } it "has correct questions" do expect(page.questions.map(&:id)).to eq(%w[proplen_asked proplen]) @@ -39,15 +41,63 @@ RSpec.describe Form::Sales::Pages::LivingBeforePurchase, type: :model do expect(page.description).to be_nil end - it "has correct depends_on" do - expect(page.depends_on).to eq([{ "not_joint_purchase?" => true }, { "jointpur" => nil }]) - end + context "when routing" do + context "with form before 2025" do + let(:start_year) { 2024 } + + context "with joint purchase" do + subject(:page) { described_class.new(page_id, page_definition, subsection, ownershipsch: 1, joint_purchase: true) } + + it "routes to the page when joint purchase is true" do + log = build(:sales_log, jointpur: 1) + expect(page.routed_to?(log, nil)).to eq(true) + end + + it "does not route to the page when joint purchase is false" do + log = build(:sales_log, jointpur: 2) + expect(page.routed_to?(log, nil)).to eq(false) + end + + it "does not route to the page when joint purchase is missing" do + log = build(:sales_log, jointpur: nil) + expect(page.routed_to?(log, nil)).to eq(false) + end + end + + context "with non joint purchase" do + subject(:page) { described_class.new(page_id, page_definition, subsection, ownershipsch: 1, joint_purchase: false) } + + it "routes to the page when joint purchase is false" do + log = build(:sales_log, jointpur: 2) + expect(page.routed_to?(log, nil)).to eq(true) + end - context "with joint purchase" do - subject(:page) { described_class.new(page_id, page_definition, subsection, ownershipsch: 1, joint_purchase: true) } + it "does not route to the page when joint purchase is true" do + log = build(:sales_log, jointpur: 1) + expect(page.routed_to?(log, nil)).to eq(false) + end - it "has correct depends_on" do - expect(page.depends_on).to eq([{ "joint_purchase?" => true }]) + it "routes to the page when joint purchase is missing" do + log = build(:sales_log, jointpur: nil) + expect(page.routed_to?(log, nil)).to eq(true) + end + end + end + + context "with form on or after 2025" do + subject(:page) { described_class.new(page_id, page_definition, subsection, ownershipsch: 1, joint_purchase: true) } + + let(:start_year) { 2025 } + + it "routes to the page when resale is 2" do + log = build(:sales_log, jointpur: 1, resale: 2) + expect(page.routed_to?(log, nil)).to eq(true) + end + + it "does not route to the page when resale is not 2" do + log = build(:sales_log, jointpur: 1, resale: nil) + expect(page.routed_to?(log, nil)).to eq(false) + end end end 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/privacy_notice_spec.rb b/spec/models/form/sales/pages/privacy_notice_spec.rb index 80be7ae66..8b21e7b9e 100644 --- a/spec/models/form/sales/pages/privacy_notice_spec.rb +++ b/spec/models/form/sales/pages/privacy_notice_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Pages::PrivacyNotice, type: :model do let(:page_id) { "privacy_notice" } let(:page_definition) { nil } - let(:subsection) { instance_double(Form::Subsection, id: "setup") } + let(:subsection) { instance_double(Form::Subsection, id: "setup", copy_key: "setup") } let(:form) { instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2024_or_later?: false) } before do @@ -31,20 +31,10 @@ RSpec.describe Form::Sales::Pages::PrivacyNotice, type: :model do context "when there are joint buyers" do subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: true) } - context "when the form start year is before 2024" do - let(:subsection) { instance_double(Form::Subsection, id: "household_characteristics") } + let(:subsection) { instance_double(Form::Subsection, id: "subsection_id", copy_key: "subsection_copy_key") } - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.household_characteristics.privacynotice.joint_purchase") - end - end - - context "when the form start year is after 2024" do - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2024_or_later?: true) } - - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.setup.privacynotice.joint_purchase") - end + it "has the expected copy_key" do + expect(page.copy_key).to eq("sales.subsection_copy_key.privacynotice.joint_purchase") end it "has correct depends_on" do @@ -55,20 +45,10 @@ RSpec.describe Form::Sales::Pages::PrivacyNotice, type: :model do context "when there is a single buyer" do subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase: false) } - context "when the form start year is before 2024" do - let(:subsection) { instance_double(Form::Subsection, id: "household_characteristics") } - - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.household_characteristics.privacynotice.not_joint_purchase") - end - end - - context "when the form start year is after 2024" do - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2024_or_later?: true) } + let(:subsection) { instance_double(Form::Subsection, id: "subsection_id", copy_key: "subsection_copy_key") } - it "has the expected copy_key" do - expect(page.copy_key).to eq("sales.setup.privacynotice.not_joint_purchase") - end + it "has the expected copy_key" do + expect(page.copy_key).to eq("sales.subsection_copy_key.privacynotice.not_joint_purchase") end it "has correct depends_on" do 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/buyer_interview_spec.rb b/spec/models/form/sales/questions/buyer_interview_spec.rb index 0db43407f..e812f6146 100644 --- a/spec/models/form/sales/questions/buyer_interview_spec.rb +++ b/spec/models/form/sales/questions/buyer_interview_spec.rb @@ -6,7 +6,8 @@ RSpec.describe Form::Sales::Questions::BuyerInterview, type: :model do let(:question_id) { nil } let(:question_definition) { nil } let(:form) { instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2024_or_later?: true) } - let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form:, id: "setup")) } + let(:subsection) { instance_double(Form::Subsection, form:, copy_key: "setup") } + let(:page) { instance_double(Form::Page, subsection:) } it "has correct page" do expect(question.page).to eq(page) @@ -34,42 +35,20 @@ RSpec.describe Form::Sales::Questions::BuyerInterview, type: :model do context "when there are joint buyers" do subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase: true) } - context "when the form start year is before 2024" do - let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form:, id: "household_characteristics")) } - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 3, 1), start_year_2024_or_later?: false) } + let(:subsection) { instance_double(Form::Subsection, form:, copy_key: "subsection_copy_key") } - it "has the expected copy_key" do - expect(question.copy_key).to eq("sales.household_characteristics.noint.joint_purchase") - end - end - - context "when the form start year is after 2024" do - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2024_or_later?: true) } - - it "has the expected copy_key" do - expect(question.copy_key).to eq("sales.setup.noint.joint_purchase") - end + it "has the expected copy_key" do + expect(question.copy_key).to eq("sales.subsection_copy_key.noint.joint_purchase") end end context "when there is a single buyer" do subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase: false) } - context "when the form start year is before 2024" do - let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form:, id: "household_characteristics")) } - let(:form) { instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2024_or_later?: false) } - - it "has the expected copy_key" do - expect(question.copy_key).to eq("sales.household_characteristics.noint.not_joint_purchase") - end - end - - context "when the form start year is after 2024" do - let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2024_or_later?: true) } + let(:subsection) { instance_double(Form::Subsection, form:, copy_key: "subsection_copy_key") } - it "has the expected copy_key" do - expect(question.copy_key).to eq("sales.setup.noint.not_joint_purchase") - end + it "has the expected copy_key" do + expect(question.copy_key).to eq("sales.subsection_copy_key.noint.not_joint_purchase") end end end 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/privacy_notice_spec.rb b/spec/models/form/sales/questions/privacy_notice_spec.rb index 5f764c7af..f279c6bf9 100644 --- a/spec/models/form/sales/questions/privacy_notice_spec.rb +++ b/spec/models/form/sales/questions/privacy_notice_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Form::Sales::Questions::PrivacyNotice, type: :model do let(:question_id) { nil } let(:question_definition) { nil } let(:page) { instance_double(Form::Page) } - let(:subsection) { instance_double(Form::Subsection, id: "setup") } + let(:subsection) { instance_double(Form::Subsection, id: "setup", copy_key: "setup") } let(:form) { instance_double(Form, start_date: Time.zone.local(2023, 4, 1)) } before do @@ -32,7 +32,7 @@ RSpec.describe Form::Sales::Questions::PrivacyNotice, type: :model do end context "when the form year is before 2024" do - let(:subsection) { instance_double(Form::Subsection, id: "household_characteristics") } + let(:subsection) { instance_double(Form::Subsection, id: "household_characteristics", copy_key: "household_characteristics") } before do allow(form).to receive(:start_year_2024_or_later?).and_return(false) 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/uprn_confirmation_spec.rb b/spec/models/form/sales/questions/uprn_confirmation_spec.rb index 0e54155b6..c6f7fd723 100644 --- a/spec/models/form/sales/questions/uprn_confirmation_spec.rb +++ b/spec/models/form/sales/questions/uprn_confirmation_spec.rb @@ -25,10 +25,6 @@ RSpec.describe Form::Sales::Questions::UprnConfirmation, type: :model do expect(question.derived?(nil)).to be false end - it "has the correct unanswered_error_message" do - expect(question.unanswered_error_message).to eq("You must answer #{format_ending(I18n.t('forms.2023.sales.property_information.uprn_confirmed.check_answer_label'))}") - end - describe "notification_banner" do context "when address is not present" do it "returns nil" do 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 0b2ab4144..191bca4bb 100644 --- a/spec/models/form/sales/sections/sale_information_spec.rb +++ b/spec/models/form/sales/sections/sale_information_spec.rb @@ -5,18 +5,41 @@ RSpec.describe Form::Sales::Sections::SaleInformation, type: :model do let(:section_id) { nil } let(:section_definition) { nil } - let(:form) { instance_double(Form) } + 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 - it "has correct subsections" do - expect(sale_information.subsections.map(&:id)).to eq(%w[ - shared_ownership_scheme - discounted_ownership_scheme - outright_sale - ]) + context "when form is before 2025" do + it "has correct subsections" do + expect(sale_information.subsections.map(&:id)).to eq(%w[ + shared_ownership_scheme + discounted_ownership_scheme + outright_sale + ]) + end + end + + context "when form is 2025 or later" do + 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 + ]) + end end it "has the correct id" do diff --git a/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb b/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb index a573103a3..3fa57571b 100644 --- a/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb +++ b/spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb @@ -56,6 +56,10 @@ RSpec.describe Form::Sales::Subsections::DiscountedOwnershipScheme, type: :model expect(discounted_ownership_scheme.id).to eq("discounted_ownership_scheme") end + it "has the correct copy key" do + expect(discounted_ownership_scheme.copy_key).to eq("sale_information") + end + it "has the correct label" do expect(discounted_ownership_scheme.label).to eq("Discounted ownership scheme") end diff --git a/spec/models/form/sales/subsections/household_characteristics_spec.rb b/spec/models/form/sales/subsections/household_characteristics_spec.rb index 3cd856eb3..d892febc4 100644 --- a/spec/models/form/sales/subsections/household_characteristics_spec.rb +++ b/spec/models/form/sales/subsections/household_characteristics_spec.rb @@ -16,116 +16,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model expect(household_characteristics.section).to eq(section) end - it "has the correct id" do - expect(household_characteristics.id).to eq("household_characteristics") - end - - it "has the correct label" do - expect(household_characteristics.label).to eq("Household characteristics") - end - - context "with 2022/23 form" do - before do - allow(form).to receive(:start_date).and_return(Time.zone.local(2022, 4, 1)) - allow(form).to receive(:start_year_2024_or_later?).and_return(false) - allow(form).to receive(:start_year_2025_or_later?).and_return(false) - end - - it "has correct pages" do - expect(household_characteristics.pages.map(&:id)).to eq( - %w[ - buyer_interview_joint_purchase - buyer_interview - privacy_notice_joint_purchase - privacy_notice - buyer_1_age - age_1_retirement_value_check - age_1_old_persons_shared_ownership_joint_purchase_value_check - age_1_old_persons_shared_ownership_value_check - buyer_1_gender_identity - buyer_1_ethnic_group - buyer_1_ethnic_background_black - buyer_1_ethnic_background_asian - buyer_1_ethnic_background_arab - buyer_1_ethnic_background_mixed - buyer_1_ethnic_background_white - buyer_1_nationality - buyer_1_working_situation - working_situation_1_retirement_value_check - working_situation_buyer_1_income_min_value_check - buyer_1_live_in_property - buyer_1_live_in_property_value_check - buyer_2_relationship_to_buyer_1 - buyer_2_relationship_student_not_child_value_check - buyer_2_age - age_2_old_persons_shared_ownership_joint_purchase_value_check - age_2_old_persons_shared_ownership_value_check - age_2_buyer_retirement_value_check - buyer_2_age_student_not_child_value_check - buyer_2_gender_identity - buyer_2_working_situation - working_situation_2_retirement_value_check_joint_purchase - working_situation_buyer_2_income_min_value_check - buyer_2_working_situation_student_not_child_value_check - buyer_2_live_in_property - buyer_2_live_in_property_value_check - number_of_others_in_property - number_of_others_in_property_joint_purchase - person_2_known - person_2_relationship_to_buyer_1 - relationship_2_student_not_child_value_check - person_2_age - age_2_retirement_value_check - age_2_student_not_child_value_check - person_2_gender_identity - person_2_working_situation - working_situation_2_retirement_value_check - working_situation_2_student_not_child_value_check - person_3_known - person_3_relationship_to_buyer_1 - relationship_3_student_not_child_value_check - person_3_age - age_3_retirement_value_check - age_3_student_not_child_value_check - person_3_gender_identity - person_3_working_situation - working_situation_3_retirement_value_check - working_situation_3_student_not_child_value_check - person_4_known - person_4_relationship_to_buyer_1 - relationship_4_student_not_child_value_check - person_4_age - age_4_retirement_value_check - age_4_student_not_child_value_check - person_4_gender_identity - person_4_working_situation - working_situation_4_retirement_value_check - working_situation_4_student_not_child_value_check - person_5_known - person_5_relationship_to_buyer_1 - relationship_5_student_not_child_value_check - person_5_age - age_5_retirement_value_check - age_5_student_not_child_value_check - person_5_gender_identity - person_5_working_situation - working_situation_5_retirement_value_check - working_situation_5_student_not_child_value_check - person_6_known - person_6_relationship_to_buyer_1 - relationship_6_student_not_child_value_check - person_6_age - age_6_retirement_value_check - age_6_student_not_child_value_check - person_6_gender_identity - person_6_working_situation - working_situation_6_retirement_value_check - working_situation_6_student_not_child_value_check - ], - ) - end - end - context "with 2023/24 form" do before do allow(form).to receive(:start_date).and_return(Time.zone.local(2023, 4, 1)) @@ -154,7 +44,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_1_nationality buyer_1_working_situation working_situation_1_retirement_value_check - working_situation_buyer_1_income_min_value_check + working_situation_buyer_1_income_value_check buyer_1_live_in_property buyer_1_live_in_property_value_check buyer_2_relationship_to_buyer_1 @@ -174,7 +64,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_2_nationality buyer_2_working_situation working_situation_2_retirement_value_check_joint_purchase - working_situation_buyer_2_income_min_value_check + working_situation_buyer_2_income_value_check buyer_2_working_situation_student_not_child_value_check buyer_2_live_in_property buyer_2_live_in_property_value_check @@ -281,7 +171,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_1_working_situation working_situation_1_retirement_value_check working_situation_1_not_retired_value_check - working_situation_buyer_1_income_min_value_check + working_situation_buyer_1_income_value_check buyer_1_live_in_property buyer_1_live_in_property_value_check buyer_2_relationship_to_buyer_1 @@ -303,7 +193,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_2_working_situation working_situation_2_retirement_value_check_joint_purchase working_situation_2_not_retired_value_check_joint_purchase - working_situation_buyer_2_income_min_value_check + working_situation_buyer_2_income_value_check buyer_2_working_situation_student_not_child_value_check buyer_2_live_in_property buyer_2_live_in_property_value_check @@ -396,10 +286,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model allow(form).to receive(:start_year_2025_or_later?).and_return(true) end - it "has correct depends on" do - expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true }]) - end - it "has correct pages" do expect(household_characteristics.pages.map(&:id)).to eq( %w[ @@ -419,7 +305,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_1_working_situation working_situation_1_retirement_value_check working_situation_1_not_retired_value_check - working_situation_buyer_1_income_min_value_check + working_situation_buyer_1_income_value_check buyer_1_live_in_property buyer_1_live_in_property_value_check buyer_2_relationship_to_buyer_1 @@ -441,7 +327,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_2_working_situation working_situation_2_retirement_value_check_joint_purchase working_situation_2_not_retired_value_check_joint_purchase - working_situation_buyer_2_income_min_value_check + working_situation_buyer_2_income_value_check buyer_2_working_situation_student_not_child_value_check buyer_2_live_in_property buyer_2_live_in_property_value_check @@ -525,5 +411,9 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model ], ) end + + it "has correct depends on" do + expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true }]) + end end end diff --git a/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb index 557155758..e6c46a119 100644 --- a/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb +++ b/spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb @@ -5,7 +5,8 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model let(:subsection_id) { nil } let(:subsection_definition) { nil } - let(:section) { instance_double(Form::Sales::Sections::Household) } + let(:section) { instance_double(Form::Sales::Sections::Household, form:) } + let(:form) { instance_double(Form, start_date: Time.utc(2024, 4, 1), start_year_2025_or_later?: false) } it "has correct section" do expect(subsection.section).to eq(section) @@ -19,88 +20,80 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model expect(subsection.label).to eq("Income, benefits and savings") end - describe "pages" do - let(:section) { instance_double(Form::Sales::Sections::Household, form: instance_double(Form, start_date:, start_year_2025_or_later?: false)) } + context "when before 2025" do + let(:form) { instance_double(Form, start_date: Time.utc(2024, 4, 1), start_year_2025_or_later?: false) } - context "when 2022" do - let(:start_date) { Time.utc(2022, 2, 8) } - - it "has correct pages" do - expect(subsection.pages.compact.map(&:id)).to eq( - %w[ - buyer_1_income - buyer_1_income_min_value_check - buyer_1_income_max_value_check - buyer_1_combined_income_max_value_check - buyer_1_income_mortgage_value_check - buyer_1_mortgage - buyer_1_mortgage_value_check - buyer_2_income - buyer_2_income_mortgage_value_check - buyer_2_income_min_value_check - buyer_2_income_max_value_check - buyer_2_combined_income_max_value_check - buyer_2_mortgage - buyer_2_mortgage_value_check - housing_benefits_joint_purchase - housing_benefits_not_joint_purchase - savings_joint_purchase - savings - savings_joint_purchase_value_check - savings_value_check - savings_deposit_joint_purchase_value_check - savings_deposit_value_check - previous_ownership_joint_purchase - previous_ownership_not_joint_purchase - ], - ) - end + it "has correct pages" do + expect(subsection.pages.map(&:id)).to eq( + %w[ + buyer_1_income + buyer_1_income_ecstat_value_check + buyer_1_income_discounted_max_value_check + buyer_1_combined_income_max_value_check + buyer_1_income_mortgage_value_check + buyer_1_mortgage + buyer_1_mortgage_value_check + buyer_2_income + buyer_2_income_mortgage_value_check + buyer_2_income_ecstat_value_check + buyer_2_income_discounted_max_value_check + buyer_2_combined_income_max_value_check + buyer_2_mortgage + buyer_2_mortgage_value_check + housing_benefits_joint_purchase + housing_benefits_not_joint_purchase + savings_joint_purchase + savings + savings_joint_purchase_value_check + savings_value_check + savings_deposit_joint_purchase_value_check + savings_deposit_value_check + previous_ownership_joint_purchase + previous_ownership_not_joint_purchase + previous_shared + ], + ) end - context "when 2023" do - let(:start_date) { Time.utc(2023, 2, 8) } - - it "has correct pages" do - expect(subsection.pages.map(&:id)).to eq( - %w[ - buyer_1_income - buyer_1_income_min_value_check - buyer_1_income_max_value_check - buyer_1_combined_income_max_value_check - buyer_1_income_mortgage_value_check - buyer_1_mortgage - buyer_1_mortgage_value_check - buyer_2_income - buyer_2_income_mortgage_value_check - buyer_2_income_min_value_check - buyer_2_income_max_value_check - buyer_2_combined_income_max_value_check - buyer_2_mortgage - buyer_2_mortgage_value_check - housing_benefits_joint_purchase - housing_benefits_not_joint_purchase - savings_joint_purchase - savings - savings_joint_purchase_value_check - savings_value_check - savings_deposit_joint_purchase_value_check - savings_deposit_value_check - previous_ownership_joint_purchase - previous_ownership_not_joint_purchase - previous_shared - ], - ) - end - - it "has correct depends on" do - expect(subsection.depends_on).to eq([{ "setup_completed?" => true }]) - end + it "has correct depends on" do + expect(subsection.depends_on).to eq([{ "setup_completed?" => true }]) end end context "when 2025" do - let(:start_date) { Time.utc(2025, 2, 8) } - let(:section) { instance_double(Form::Sales::Sections::Household, form: instance_double(Form, start_date:, start_year_2025_or_later?: true)) } + let(:form) { instance_double(Form, start_date: Time.utc(2025, 4, 1), start_year_2025_or_later?: true) } + + it "has correct pages" do + expect(subsection.pages.map(&:id)).to eq( + %w[ + buyer_1_income + buyer_1_income_ecstat_value_check + buyer_1_income_discounted_max_value_check + buyer_1_combined_income_max_value_check + buyer_1_income_mortgage_value_check + buyer_1_mortgage + buyer_1_mortgage_value_check + buyer_2_income + buyer_2_income_mortgage_value_check + buyer_2_income_ecstat_value_check + buyer_2_income_discounted_max_value_check + buyer_2_combined_income_max_value_check + buyer_2_mortgage + buyer_2_mortgage_value_check + housing_benefits_joint_purchase + housing_benefits_not_joint_purchase + savings_joint_purchase + savings + savings_joint_purchase_value_check + savings_value_check + savings_deposit_joint_purchase_value_check + savings_deposit_value_check + previous_ownership_joint_purchase + previous_ownership_not_joint_purchase + previous_shared + ], + ) + end it "has correct depends on" do expect(subsection.depends_on).to eq([{ "setup_completed?" => true, "is_staircase?" => false }]) diff --git a/spec/models/form/sales/subsections/outright_sale_spec.rb b/spec/models/form/sales/subsections/outright_sale_spec.rb index 52941d0a9..b91553939 100644 --- a/spec/models/form/sales/subsections/outright_sale_spec.rb +++ b/spec/models/form/sales/subsections/outright_sale_spec.rb @@ -109,6 +109,10 @@ RSpec.describe Form::Sales::Subsections::OutrightSale, type: :model do expect(outright_sale.id).to eq("outright_sale") end + it "has the correct copy key" do + expect(outright_sale.copy_key).to eq("sale_information") + end + it "has the correct label" do expect(outright_sale.label).to eq("Outright sale") end diff --git a/spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb b/spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb new file mode 100644 index 000000000..3e473a162 --- /dev/null +++ b/spec/models/form/sales/subsections/shared_ownership_initial_purchase_spec.rb @@ -0,0 +1,95 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Subsections::SharedOwnershipInitialPurchase, type: :model do + subject(:shared_ownership_initial_purchase) { described_class.new(subsection_id, subsection_definition, section) } + + let(:subsection_id) { nil } + let(:subsection_definition) { nil } + let(:section) { instance_double(Form::Sales::Sections::SaleInformation) } + + before do + allow(section).to receive(:form).and_return(instance_double(Form, start_date: Time.zone.local(2025, 4, 1))) + end + + it "has correct section" do + expect(shared_ownership_initial_purchase.section).to eq(section) + end + + it "has correct pages" do + expect(shared_ownership_initial_purchase.pages.map(&:id)).to eq( + %w[ + resale + living_before_purchase_shared_ownership_joint_purchase + living_before_purchase_shared_ownership + handover_date + handover_date_check + buyer_previous_joint_purchase + buyer_previous_not_joint_purchase + previous_bedrooms + previous_property_type + shared_ownership_previous_tenure + value_shared_ownership + about_price_shared_ownership_value_check + initial_equity + shared_ownership_equity_value_check + mortgage_used_shared_ownership + mortgage_used_mortgage_value_check + mortgage_amount_shared_ownership + shared_ownership_mortgage_amount_value_check + mortgage_amount_mortgage_value_check + mortgage_length_shared_ownership + deposit_shared_ownership + deposit_shared_ownership_optional + deposit_joint_purchase_value_check + deposit_value_check + deposit_discount + deposit_discount_optional + shared_ownership_deposit_value_check + monthly_rent + leasehold_charges_shared_ownership + monthly_charges_shared_ownership_value_check + estate_management_fee + ], + ) + end + + it "has the correct id" do + expect(shared_ownership_initial_purchase.id).to eq("shared_ownership_initial_purchase") + end + + it "has the correct label" do + expect(shared_ownership_initial_purchase.label).to eq("Shared ownership - initial purchase") + end + + it "has the correct depends_on" do + expect(shared_ownership_initial_purchase.depends_on).to eq([ + { + "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 2 + }, + ]) + end + + context "when it is a shared ownership scheme and not staircase" do + let(:log) { FactoryBot.build(:sales_log, ownershipsch: 1, staircase: 2) } + + it "is displayed in tasklist" do + expect(shared_ownership_initial_purchase.displayed_in_tasklist?(log)).to eq(true) + end + end + + context "when it is not a shared ownership scheme" do + let(:log) { FactoryBot.build(:sales_log, ownershipsch: 2, staircase: 2) } + + it "is displayed in tasklist" do + expect(shared_ownership_initial_purchase.displayed_in_tasklist?(log)).to eq(false) + end + end + + context "when it is staircase" do + let(:log) { FactoryBot.build(:sales_log, ownershipsch: 1, staircase: 1) } + + it "is displayed in tasklist" do + expect(shared_ownership_initial_purchase.displayed_in_tasklist?(log)).to eq(false) + end + end +end diff --git a/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb b/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb index 4c546d58c..51e2ee60d 100644 --- a/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb +++ b/spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb @@ -65,6 +65,10 @@ RSpec.describe Form::Sales::Subsections::SharedOwnershipScheme, type: :model do expect(shared_ownership_scheme.id).to eq("shared_ownership_scheme") end + it "has the correct copy key" do + expect(shared_ownership_scheme.copy_key).to eq("sale_information") + end + it "has the correct label" do expect(shared_ownership_scheme.label).to eq("Shared ownership scheme") end diff --git a/spec/models/form/subsection_spec.rb b/spec/models/form/subsection_spec.rb index 8a4b9d3d7..0215e51dc 100644 --- a/spec/models/form/subsection_spec.rb +++ b/spec/models/form/subsection_spec.rb @@ -19,6 +19,10 @@ RSpec.describe Form::Subsection, type: :model do expect(subsection.id).to eq(subsection_id) end + it "has a copy_key defaulting to the id" do + expect(subsection.copy_key).to eq(subsection_id) + end + it "has a label" do expect(subsection.label).to eq("Household characteristics") end diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb index d3dc7e21f..74594c9b9 100644 --- a/spec/models/form_spec.rb +++ b/spec/models/form_spec.rb @@ -329,7 +329,7 @@ RSpec.describe Form, type: :model do end context "when a value is changed such that a radio and free input questions are no longer routed to" do - let(:log) { FactoryBot.create(:lettings_log, :completed, startdate: now) } + let(:log) { FactoryBot.create(:lettings_log, :completed, startdate: now, hhmemb: 2, details_known_2: 0, sex2: "M", relat2: "P", age2_known: 0, age2: 32, ecstat2: 6) } it "all attributes relating to that checkbox question are cleared" do expect(log.hhmemb).to be 2 diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index c47de8cf6..11e663469 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -809,6 +809,21 @@ RSpec.describe LettingsLog do expect { lettings_log.update!(nationality_all_group: nil, declaration: 1) }.not_to change(lettings_log, :nationality_all) end end + + context "when form year changes and LA is no longer active" do + before do + LocalAuthority.find_by(code: "E08000003").update!(end_date: Time.zone.today) + end + + it "removes the LA" do + lettings_log.update!(startdate: Time.zone.yesterday, la: "E08000003") + expect(lettings_log.reload.la).to eq("E08000003") + + lettings_log.update!(startdate: Time.zone.tomorrow) + expect(lettings_log.reload.la).to eq(nil) + expect(lettings_log.reload.is_la_inferred).to eq(false) + end + end end describe "optional fields" do @@ -2006,5 +2021,17 @@ RSpec.describe LettingsLog do end end end + + describe "#process_address_change!" do + context "when uprn_selection is uprn_not_listed" do + let(:log) { build(:lettings_log, uprn_selection: "uprn_not_listed", address_line1_input: "Address line 1", postcode_full_input: "AA1 1AA") } + + it "sets log address fields, including postcode known" do + expect { log.process_address_change! }.to change(log, :address_line1).from(nil).to("Address line 1") + .and change(log, :postcode_full).from(nil).to("AA1 1AA") + .and change(log, :postcode_known).from(nil).to(1) + end + end + end end # rubocop:enable RSpec/MessageChain diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 79265d361..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 @@ -929,6 +957,11 @@ RSpec.describe Location, type: :model do expect(location.status).to eq(:deactivated) end + it "returns deactivated if the owning organisation has been merged" do + location.scheme.owning_organisation.merge_date = 2.days.ago + expect(location.status).to eq(:deactivated) + end + it "returns deactivated if deactivation_date is in the past" do FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.yesterday, location:) location.save! diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index fe919a336..9b01845ae 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -207,7 +207,7 @@ RSpec.describe Organisation, type: :model do end describe "paper trail" do - let(:organisation) { create(:organisation) } + let(:organisation) { create(:organisation, name: "MHCLG") } it "creates a record of changes to a log" do expect { organisation.update!(name: "new test name") }.to change(organisation.versions, :count).by(1) diff --git a/spec/models/sales_log_spec.rb b/spec/models/sales_log_spec.rb index 1bd091f43..f121e9d1a 100644 --- a/spec/models/sales_log_spec.rb +++ b/spec/models/sales_log_spec.rb @@ -1027,5 +1027,34 @@ RSpec.describe SalesLog, type: :model do end end end + + context "when form year changes and LA is no longer active" do + let!(:sales_log) { create(:sales_log) } + + before do + LocalAuthority.find_by(code: "E08000003").update!(end_date: Time.zone.today) + end + + it "removes the LA" do + sales_log.update!(saledate: Time.zone.yesterday, la: "E08000003") + expect(sales_log.reload.la).to eq("E08000003") + + sales_log.update!(saledate: Time.zone.tomorrow) + expect(sales_log.reload.la).to eq(nil) + expect(sales_log.reload.is_la_inferred).to eq(false) + end + end + + describe "#process_address_change!" do + context "when uprn_selection is uprn_not_listed" do + let(:log) { build(:sales_log, uprn_selection: "uprn_not_listed", address_line1_input: "Address line 1", postcode_full_input: "AA1 1AA") } + + it "sets log address fields, including postcode known" do + expect { log.process_address_change! }.to change(log, :address_line1).from(nil).to("Address line 1") + .and change(log, :postcode_full).from(nil).to("AA1 1AA") + .and change(log, :pcodenk).from(nil).to(0) + end + end + end end # rubocop:enable RSpec/MessageChain diff --git a/spec/models/scheme_spec.rb b/spec/models/scheme_spec.rb index 5ca529d3e..65174388d 100644 --- a/spec/models/scheme_spec.rb +++ b/spec/models/scheme_spec.rb @@ -363,6 +363,11 @@ RSpec.describe Scheme, type: :model do expect(scheme.status).to eq(:deactivated) end + it "returns deactivated if the owning organisation has been merged" do + scheme.owning_organisation.merge_date = 2.days.ago + expect(scheme.status).to eq(:deactivated) + end + it "returns deactivated if deactivation_date is in the past" do FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.yesterday, scheme:) scheme.reload diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 89f4f9dee..51cfc00bd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -380,7 +380,7 @@ RSpec.describe User, type: :model do end describe "paper trail" do - let(:user) { create(:user) } + let(:user) { create(:user, name: "Danny Rojas") } it "creates a record of changes to a log" do expect { user.update!(name: "new test name") }.to change(user.versions, :count).by(1) 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/sales/soft_validations_spec.rb b/spec/models/validations/sales/soft_validations_spec.rb index 99c615250..51f0d695a 100644 --- a/spec/models/validations/sales/soft_validations_spec.rb +++ b/spec/models/validations/sales/soft_validations_spec.rb @@ -3,85 +3,222 @@ require "rails_helper" RSpec.describe Validations::Sales::SoftValidations do let(:record) { build(:sales_log) } - describe "income1 min validations" do - context "when validating soft min" do - it "returns false if no income1 is given" do + describe "income validations" do + context "when validating soft range based on ecstat" do + it "does not trigger for income1 if no income1 is given" do record.income1 = nil - - expect(record).not_to be_income1_under_soft_min + expect(record).not_to be_income1_outside_soft_range_for_ecstat end - it "returns false if no ecstat1 is given" do + it "does not trigger for low income1 if no ecstat1 is given" do + record.income1 = 50 record.ecstat1 = nil + expect(record).not_to be_income1_outside_soft_range_for_ecstat + end - expect(record).not_to be_income1_under_soft_min + it "does not trigger for income2 if no income2 is given" do + record.income2 = nil + expect(record).not_to be_income2_outside_soft_range_for_ecstat end - [ - { - income1: 4500, - ecstat1: 1, - }, - { - income1: 1400, - ecstat1: 2, - }, - { - income1: 999, - ecstat1: 3, - }, - { - income1: 1899, - ecstat1: 5, - }, - { - income1: 1888, - ecstat1: 0, - }, - ].each do |test_case| - it "returns true if income1 is below soft min for ecstat1 #{test_case[:ecstat1]}" do - record.income1 = test_case[:income1] - record.ecstat1 = test_case[:ecstat1] - expect(record) - .to be_income1_under_soft_min + it "does not trigger for low income2 if no ecstat2 is given" do + record.income2 = 50 + record.ecstat2 = nil + expect(record).not_to be_income2_outside_soft_range_for_ecstat + end + + context "when log year is before 2025" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2024, 12, 25)) } + + it "does not trigger for low income1 if ecstat1 has no soft min" do + record.income1 = 50 + record.ecstat1 = 4 + expect(record).not_to be_income1_outside_soft_range_for_ecstat + end + + it "returns true if income1 is below soft min for ecstat1" do + record.income1 = 4500 + record.ecstat1 = 1 + expect(record).to be_income1_outside_soft_range_for_ecstat + end + + it "returns false if income1 is >= soft min for ecstat1" do + record.income1 = 1500 + record.ecstat1 = 2 + expect(record).not_to be_income1_outside_soft_range_for_ecstat + end + + it "does not trigger for income2 if ecstat2 has no soft min" do + record.income2 = 50 + record.ecstat2 = 8 + expect(record).not_to be_income2_outside_soft_range_for_ecstat + end + + it "returns true if income2 is below soft min for ecstat2" do + record.income2 = 999 + record.ecstat2 = 3 + expect(record).to be_income2_outside_soft_range_for_ecstat + end + + it "returns false if income2 is >= soft min for ecstat2" do + record.income2 = 2500 + record.ecstat2 = 5 + expect(record).not_to be_income2_outside_soft_range_for_ecstat + end + + it "does not trigger for being over maxima" do + record.ecstat1 = 1 + record.income1 = 200_000 + record.ecstat2 = 2 + record.income2 = 100_000 + expect(record).not_to be_income1_outside_soft_range_for_ecstat + expect(record).not_to be_income2_outside_soft_range_for_ecstat end end - [ - { - income1: 5001, - ecstat1: 1, - }, - { - income1: 1600, - ecstat1: 2, - }, - { - income1: 1004, - ecstat1: 3, - }, - { - income1: 2899, - ecstat1: 4, - }, - { - income1: 2899, - ecstat1: 5, - }, - { - income1: 5, - ecstat1: 6, - }, - { - income1: 10_888, - ecstat1: 0, - }, - ].each do |test_case| - it "returns false if income1 is over soft min for ecstat1 #{test_case[:ecstat1]}" do - record.income1 = test_case[:income1] - record.ecstat1 = test_case[:ecstat1] - expect(record) - .not_to be_income1_under_soft_min + context "when log year is 2025" do + let(:record) { build(:sales_log, saledate: Time.zone.local(2025, 12, 25)) } + + it "returns true if income1 is below soft min for ecstat1" do + record.income1 = 13_399 + record.ecstat1 = 1 + expect(record).to be_income1_outside_soft_range_for_ecstat + end + + it "returns false if income1 is >= soft min for ecstat1" do + record.income1 = 2600 + record.ecstat1 = 2 + expect(record).not_to be_income1_outside_soft_range_for_ecstat + end + + it "returns true if income2 is below soft min for ecstat2" do + record.income2 = 2079 + record.ecstat2 = 3 + expect(record).to be_income2_outside_soft_range_for_ecstat + end + + it "returns false if income2 is >= soft min for ecstat2" do + record.income2 = 520 + record.ecstat2 = 5 + expect(record).not_to be_income2_outside_soft_range_for_ecstat + end + + it "returns true if income1 is above soft max for ecstat1" do + record.income1 = 80_001 + record.ecstat1 = 2 + expect(record).to be_income1_outside_soft_range_for_ecstat + end + + it "returns false if income1 is <= soft max for ecstat1" do + record.income1 = 80_000 + record.ecstat1 = 2 + expect(record).not_to be_income1_outside_soft_range_for_ecstat + end + + it "returns true if income2 is above soft max for ecstat2" do + record.income2 = 50_001 + record.ecstat2 = 6 + expect(record).to be_income2_outside_soft_range_for_ecstat + end + + it "returns false if income2 is <= soft max for ecstat2" do + record.income2 = 50_000 + record.ecstat2 = 6 + expect(record).not_to be_income2_outside_soft_range_for_ecstat + end + end + end + + context "when validating soft max for discounted ownership" do + it "does not trigger if la is not set" do + record.la = nil + record.income1 = 100_000 + record.income2 = 95_000 + record.ownershipsch = 2 + expect(record).not_to be_income1_over_soft_max_for_discounted_ownership + expect(record).not_to be_income2_over_soft_max_for_discounted_ownership + expect(record).not_to be_combined_income_over_soft_max_for_discounted_ownership + end + + it "does not trigger if sale is not discounted ownership" do + record.la = "E09000001" + record.income1 = 100_000 + record.income2 = 95_000 + record.ownershipsch = 1 + expect(record).not_to be_income1_over_soft_max_for_discounted_ownership + expect(record).not_to be_income2_over_soft_max_for_discounted_ownership + expect(record).not_to be_combined_income_over_soft_max_for_discounted_ownership + end + + context "when property is in London for a discounted ownership sale" do + let(:record) { build(:sales_log, ownershipsch: 2, la: "E09000001") } + + it "returns true for income1 if income1 > London threshold" do + record.income1 = 90_001 + expect(record).to be_income1_over_soft_max_for_discounted_ownership + end + + it "returns false for income1 if income1 <= London threshold" do + record.income1 = 90_000 + expect(record).not_to be_income1_over_soft_max_for_discounted_ownership + end + + it "returns true for income2 if income2 > London threshold" do + record.income2 = 100_000 + expect(record).to be_income2_over_soft_max_for_discounted_ownership + end + + it "returns false for income2 if income2 <= London threshold" do + record.income2 = 30_000 + expect(record).not_to be_income2_over_soft_max_for_discounted_ownership + end + + it "returns true for combined income if > London threshold" do + record.income1 = 61_000 + record.income2 = 30_000 + expect(record).to be_combined_income_over_soft_max_for_discounted_ownership + end + + it "returns false for combined income if <= London threshold" do + record.income1 = 40_000 + record.income2 = 20_000 + expect(record).not_to be_combined_income_over_soft_max_for_discounted_ownership + end + end + + context "when property is not in London for a discounted ownership sale" do + let(:record) { build(:sales_log, ownershipsch: 2, la: "E08000001") } + + it "returns true for income1 if income1 > non-London threshold" do + record.income1 = 80_001 + expect(record).to be_income1_over_soft_max_for_discounted_ownership + end + + it "returns false for income1 if income1 <= non-London threshold" do + record.income1 = 80_000 + expect(record).not_to be_income1_over_soft_max_for_discounted_ownership + end + + it "returns true for income2 if income2 > non-London threshold" do + record.income2 = 85_000 + expect(record).to be_income2_over_soft_max_for_discounted_ownership + end + + it "returns false for income2 if income2 <= non-London threshold" do + record.income2 = 30_000 + expect(record).not_to be_income2_over_soft_max_for_discounted_ownership + end + + it "returns true for combined income if > non-London threshold" do + record.income1 = 61_000 + record.income2 = 20_000 + expect(record).to be_combined_income_over_soft_max_for_discounted_ownership + end + + it "returns false for combined income if <= non-London threshold" do + record.income1 = 40_000 + record.income2 = 20_000 + expect(record).not_to be_combined_income_over_soft_max_for_discounted_ownership 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/csv_downloads_controller_spec.rb b/spec/requests/csv_downloads_controller_spec.rb new file mode 100644 index 000000000..982077a12 --- /dev/null +++ b/spec/requests/csv_downloads_controller_spec.rb @@ -0,0 +1,136 @@ +require "rails_helper" + +RSpec.describe CsvDownloadsController, type: :request do + describe "GET #show" do + let(:page) { Capybara::Node::Simple.new(response.body) } + let(:csv_user) { create(:user) } + let(:csv_download) { create(:csv_download, user: csv_user, organisation: csv_user.organisation) } + let(:get_file_io) do + io = StringIO.new + io.write("hello") + io.rewind + io + end + let(:mock_storage_service) { instance_double(Storage::S3Service, get_file_io:, get_presigned_url: "https://example.com") } + + before do + allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) + end + + context "when user is not signed in" do + it "redirects to sign in page" do + get "/csv-downloads/#{csv_download.id}" + expect(response).to redirect_to("/account/sign-in") + end + end + + context "when user is signed in" do + before do + sign_in user + end + + context "and the user is from a different organisation" do + let(:user) { create(:user) } + + before do + get "/csv-downloads/#{csv_download.id}" + end + + it "returns page not found" do + expect(response).to have_http_status(:unauthorized) + end + end + + context "and is the user who generated the csv" do + let(:user) { csv_user } + + before do + get "/csv-downloads/#{csv_download.id}" + end + + it "allows downloading the csv" do + expect(response).to have_http_status(:ok) + expect(page).to have_link("Download CSV", href: "/csv-downloads/#{csv_download.id}/download") + end + end + + context "and is the user is from the same organisation" do + let(:user) { create(:user, organisation: csv_user.organisation) } + + before do + get "/csv-downloads/#{csv_download.id}" + end + + it "allows downloading the csv" do + expect(response).to have_http_status(:ok) + expect(page).to have_link("Download CSV", href: "/csv-downloads/#{csv_download.id}/download") + end + end + end + end + + describe "GET #download" do + let(:csv_user) { create(:user) } + let(:csv_download) { create(:csv_download, user: csv_user, organisation: csv_user.organisation) } + let(:get_file_io) do + io = StringIO.new + io.write("hello") + io.rewind + io + end + let(:mock_storage_service) { instance_double(Storage::S3Service, get_file_io:, get_presigned_url: "https://example.com") } + + before do + allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) + end + + context "when user is not signed in" do + it "redirects to sign in page" do + get "/csv-downloads/#{csv_download.id}/download" + expect(response).to redirect_to("/account/sign-in") + end + end + + context "when user is signed in" do + before do + sign_in user + end + + context "and the user is from a different organisation" do + let(:user) { create(:user) } + + before do + get "/csv-downloads/#{csv_download.id}/download" + end + + it "returns page not found" do + expect(response).to have_http_status(:unauthorized) + end + end + + context "and is the user who generated the csv" do + let(:user) { csv_user } + + before do + get "/csv-downloads/#{csv_download.id}/download" + end + + it "allows downloading the csv" do + expect(response).to have_http_status(:found) + end + end + + context "and is the user is from the same organisation" do + let(:user) { create(:user, organisation: csv_user.organisation) } + + before do + get "/csv-downloads/#{csv_download.id}/download" + end + + it "allows downloading the csv" do + expect(response).to have_http_status(:found) + end + end + end + end +end diff --git a/spec/requests/merge_requests_controller_spec.rb b/spec/requests/merge_requests_controller_spec.rb index dc1dd817d..074d2186c 100644 --- a/spec/requests/merge_requests_controller_spec.rb +++ b/spec/requests/merge_requests_controller_spec.rb @@ -49,7 +49,7 @@ RSpec.describe MergeRequestsController, type: :request do end it "shows the correct content" do - expect(page).to have_content("Which organisations are merging into MHCLG?") + expect(page).to have_content("Which organisations are merging into #{organisation.name}?") end end end @@ -64,7 +64,7 @@ RSpec.describe MergeRequestsController, type: :request do it "adds merging organisation to the page" do merge_request.reload - expect(page).to have_content("MHCLG") + expect(page).to have_content(organisation.name) expect(page).to have_content("Other Test Org") expect(page).to have_link("Remove") end @@ -84,6 +84,22 @@ RSpec.describe MergeRequestsController, type: :request do end end + context "when the user updates merge request with organisation that is already part of another merge" do + let(:another_organisation) { create(:organisation) } + let(:other_merge_request) { create(:merge_request, merge_date: Time.zone.local(2022, 5, 4)) } + let(:params) { { merge_request: { merging_organisation: another_organisation.id, new_merging_org_ids: [] } } } + + before do + MergeRequestOrganisation.create!(merge_request_id: other_merge_request.id, merging_organisation_id: another_organisation.id) + patch "/merge-request/#{merge_request.id}/merging-organisations", headers:, params: + end + + it "displays the page with an error message" do + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content("Another merge request records #{another_organisation.name} as merging into #{other_merge_request.absorbing_organisation&.name} on 4 May 2022. Select another organisation or remove this organisation from the other merge request.") + end + end + context "when the user selects an organisation that is a part of another merge" do let(:another_organisation) { create(:organisation) } let(:params) { { merge_request: { merging_organisation: another_organisation.id, new_merging_org_ids: [] } } } @@ -244,7 +260,7 @@ RSpec.describe MergeRequestsController, type: :request do end it "shows the correct content" do - expect(page).to have_content("Which helpdesk ticket reported this merge?") + expect(page).to have_content("Was this merge reported by a helpdesk ticket?") expect(page).to have_link("Back", href: existing_absorbing_organisation_merge_request_path(merge_request)) expect(page).to have_button("Save and continue") end @@ -396,6 +412,24 @@ RSpec.describe MergeRequestsController, type: :request do }.from(nil).to(Time.zone.local(2022, 4, 10)) end end + + context "when merge date set to a date more than 1 year in the future" do + let(:merge_request) { MergeRequest.create!(requesting_organisation: organisation) } + let(:params) do + { merge_request: { page: "merge_date", "merge_date(3i)": (Time.zone.now.day + 1).to_s, "merge_date(2i)": Time.zone.now.month.to_s, "merge_date(1i)": (Time.zone.now.year + 1).to_s } } + end + + let(:request) do + patch "/merge-request/#{merge_request.id}", headers:, params: + end + + it "displays the page with an error message" do + request + + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content("The merge date must not be later than a year from today’s date.") + end + end end describe "from merging_organisations page" do @@ -442,6 +476,82 @@ RSpec.describe MergeRequestsController, type: :request do end end end + + describe "from helpdesk_ticket page" do + context "when not answering the question" do + let(:merge_request) { MergeRequest.create!(requesting_organisation: organisation, absorbing_organisation: other_organisation) } + let(:params) do + { merge_request: { page: "helpdesk_ticket" } } + end + let(:request) do + patch "/merge-request/#{merge_request.id}", headers:, params: + end + + it "renders the error" do + request + + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content("You must answer was this merge reported by a helpdesk ticket?") + end + + it "does not update the request" do + expect { request }.not_to(change { merge_request.reload.attributes }) + end + end + + context "when has_helpdesk_ticket is true but no ticket is given" do + let(:merge_request) { MergeRequest.create!(requesting_organisation: organisation, absorbing_organisation: other_organisation) } + let(:params) do + { merge_request: { page: "helpdesk_ticket", has_helpdesk_ticket: true } } + end + let(:request) do + patch "/merge-request/#{merge_request.id}", headers:, params: + end + + it "renders the error" do + request + + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content("You must answer the ticket number") + end + + it "does not update the request" do + expect { request }.not_to(change { merge_request.reload.attributes }) + end + end + + context "when has_helpdesk_ticket is false" do + let(:merge_request) { MergeRequest.create!(requesting_organisation: organisation, absorbing_organisation: other_organisation, helpdesk_ticket: "123") } + let(:params) do + { merge_request: { page: "helpdesk_ticket", has_helpdesk_ticket: false } } + end + let(:request) do + patch "/merge-request/#{merge_request.id}", headers:, params: + end + + it "updates has_helpdesk_ticket and clears helpdesk_ticket" do + request + expect(merge_request.reload.has_helpdesk_ticket).to eq(false) + expect(merge_request.helpdesk_ticket).to eq(nil) + end + end + + context "when has_helpdesk_ticket is true and ticket is given" do + let(:merge_request) { MergeRequest.create!(requesting_organisation: organisation, absorbing_organisation: other_organisation) } + let(:params) do + { merge_request: { page: "helpdesk_ticket", has_helpdesk_ticket: true, helpdesk_ticket: "321" } } + end + let(:request) do + patch "/merge-request/#{merge_request.id}", headers:, params: + end + + it "updates has_helpdesk_ticket and clears helpdesk_ticket" do + request + expect(merge_request.reload.has_helpdesk_ticket).to eq(true) + expect(merge_request.helpdesk_ticket).to eq("321") + end + end + end end describe "#merge_start_confirmation" do @@ -623,7 +733,7 @@ RSpec.describe MergeRequestsController, type: :request do it "shows user outcomes after merge" do expect(page).to have_link("View all 4 Organisation with some users users (opens in a new tab)", href: users_organisation_path(organisation_with_some_users)) expect(page).to have_link("View all 12 Organisation with many users users (opens in a new tab)", href: users_organisation_path(organisation_with_some_more_users)) - expect(page).to have_link("View all 3 MHCLG users (opens in a new tab)", href: users_organisation_path(organisation)) + expect(page).to have_link("View all 3 #{organisation.name} users (opens in a new tab)", href: users_organisation_path(organisation)) expect(page).to have_content("Organisation with no users and Organisation with no users too have no users.") expect(page).to have_content("19 users after merge") end @@ -650,7 +760,7 @@ RSpec.describe MergeRequestsController, type: :request do it "shows scheme outcomes after merge" do expect(page).to have_link("View all 4 Organisation with some schemes schemes (opens in a new tab)", href: schemes_organisation_path(organisation_with_some_schemes)) expect(page).to have_link("View all 6 Organisation with many schemes schemes (opens in a new tab)", href: schemes_organisation_path(organisation_with_some_more_schemes)) - expect(page).to have_link("View all 3 MHCLG schemes (opens in a new tab)", href: schemes_organisation_path(organisation)) + expect(page).to have_link("View all 3 #{organisation.name} schemes (opens in a new tab)", href: schemes_organisation_path(organisation)) expect(page).to have_content("Organisation with no schemes and Organisation with no schemes too have no schemes.") expect(page).to have_content("13 schemes after merge") end @@ -676,8 +786,8 @@ RSpec.describe MergeRequestsController, type: :request do it "shows logs outcomes after merge" do expect(page).to have_link("View all 4 Organisation with some logs lettings logs (opens in a new tab)", href: lettings_logs_organisation_path(organisation_with_some_logs)) expect(page).to have_link("View all 2 Organisation with some logs sales logs (opens in a new tab)", href: sales_logs_organisation_path(organisation_with_some_logs)) - expect(page).to have_link("View all 2 MHCLG lettings logs (opens in a new tab)", href: lettings_logs_organisation_path(organisation)) - expect(page).to have_link("View all 3 MHCLG sales logs (opens in a new tab)", href: sales_logs_organisation_path(organisation)) + expect(page).to have_link("View all 2 #{organisation.name} lettings logs (opens in a new tab)", href: lettings_logs_organisation_path(organisation)) + expect(page).to have_link("View all 3 #{organisation.name} sales logs (opens in a new tab)", href: sales_logs_organisation_path(organisation)) expect(page).to have_content("Organisation with no logs and Organisation with no logs too have no lettings logs.") expect(page).to have_content("Organisation with no logs and Organisation with no logs too have no sales logs.") expect(page).to have_content("6 lettings logs after merge") diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index 5a300c56c..c99ad3cfb 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -1431,7 +1431,7 @@ RSpec.describe OrganisationsController, type: :request do end context "when a search parameter is passed" do - let!(:matching_user) { create(:user, organisation:, name: "joe", email: "matching@example.com") } + let!(:matching_user) { create(:user, organisation:, name: "abcdefghijklmnopqrstuvwxyz", email: "matching@example.com") } let(:org_user_count) { User.where(organisation:).count } before do @@ -1439,7 +1439,7 @@ RSpec.describe OrganisationsController, type: :request do end context "when our search string matches case" do - let(:search_param) { "joe" } + let(:search_param) { "abcdefghijklmnopqrstuvwxyz" } it "returns only matching results" do expect(page).to have_content(matching_user.name) @@ -1459,7 +1459,7 @@ RSpec.describe OrganisationsController, type: :request do end context "when we need case insensitive search" do - let(:search_param) { "Joe" } + let(:search_param) { "Abcdefghijklmnopqrstuvwxyz" } it "returns only matching results" do expect(page).to have_content(matching_user.name) @@ -1643,6 +1643,11 @@ RSpec.describe OrganisationsController, type: :request do context "when search results require pagination" do let(:search_param) { "MHCLG" } + before do + create_list(:organisation, 27, name: "MHCLG") + get "/organisations?search=#{search_param}" + end + it "has search and pagination in the title" do expect(page).to have_title("Organisations (27 organisations matching ‘#{search_param}’) (page 1 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK") end diff --git a/spec/requests/schemes_controller_spec.rb b/spec/requests/schemes_controller_spec.rb index 19ede5cc4..83ba11fd9 100644 --- a/spec/requests/schemes_controller_spec.rb +++ b/spec/requests/schemes_controller_spec.rb @@ -89,9 +89,47 @@ RSpec.describe SchemesController, type: :request do end end + context "when a recently absorbed organisation has schemes" do + let(:absorbed_org) { create(:organisation) } + let!(:absorbed_org_schemes) { create_list(:scheme, 2, owning_organisation: absorbed_org) } + + before do + absorbed_org.merge_date = 2.days.ago + absorbed_org.absorbing_organisation = user.organisation + absorbed_org.save! + end + + it "shows absorbed organisation schemes" do + get "/schemes" + follow_redirect! + absorbed_org_schemes.each do |scheme| + expect(page).to have_content(scheme.id_to_display) + end + end + end + + context "when a non-recently absorbed organisation has schemes" do + let(:absorbed_org) { create(:organisation) } + let!(:absorbed_org_schemes) { create_list(:scheme, 2, owning_organisation: absorbed_org) } + + before do + absorbed_org.merge_date = 2.years.ago + absorbed_org.absorbing_organisation = user.organisation + absorbed_org.save! + end + + it "shows absorbed organisation schemes" do + get "/schemes" + follow_redirect! + absorbed_org_schemes.each do |scheme| + expect(page).not_to have_content(scheme.id_to_display) + end + end + end + context "when filtering" do context "with owning organisation filter" do - context "when user org does not have owning orgs" do + context "when user org does not have owning orgs or recently absorbed orgs" do it "does not show filter" do expect(page).not_to have_content("Owned by") end @@ -700,6 +738,27 @@ RSpec.describe SchemesController, type: :request do end end + context "when coordinator attempts to see scheme belonging to a recently absorbed organisation" do + let(:absorbed_organisation) { create(:organisation) } + let!(:specific_scheme) { create(:scheme, owning_organisation: absorbed_organisation) } + + before do + absorbed_organisation.merge_date = 2.days.ago + absorbed_organisation.absorbing_organisation = user.organisation + absorbed_organisation.save! + + get "/schemes/#{specific_scheme.id}" + end + + it "shows the scheme" do + expect(page).to have_content(specific_scheme.id_to_display) + end + + it "allows editing" do + expect(page).to have_link("Change") + end + end + context "when the scheme has all details but no confirmed locations" do it "shows the scheme as incomplete with text to explain" do get scheme_path(specific_scheme) @@ -1146,6 +1205,31 @@ RSpec.describe SchemesController, type: :request do end end end + + context "when making a scheme in an organisation recently absorbed by the users organisation" do + let(:absorbed_organisation) { create(:organisation) } + let(:params) do + { scheme: { service_name: " testy ", + sensitive: "1", + scheme_type: "Foyer", + registered_under_care_act: "No", + owning_organisation_id: absorbed_organisation.id, + arrangement_type: "D" } } + end + + before do + absorbed_organisation.merge_date = 2.days.ago + absorbed_organisation.absorbing_organisation = user.organisation + absorbed_organisation.save! + end + + it "creates a new scheme for this organisation and renders correct page" do + expect { post "/schemes", params: }.to change(Scheme, :count).by(1) + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_content("What client group is this scheme intended for?") + end + end end context "when signed in as a support user" do diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 1b62196bb..12418d532 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -1043,6 +1043,9 @@ RSpec.describe UsersController, type: :request do it "invites a new user" do expect { request }.to change(User, :count).by(1) + follow_redirect! + expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") + expect(page).to have_content("Invitation sent to new_user@example.com") end it "sends an invitation email" do @@ -1261,8 +1264,8 @@ RSpec.describe UsersController, type: :request do end context "when user is signed in as a support user" do - let(:user) { create(:user, :support, organisation: create(:organisation, :without_dpc)) } - let(:other_user) { create(:user, organisation: user.organisation, last_sign_in_at: Time.zone.now) } + let(:user) { create(:user, :support, name: "Danny Rojas", organisation: create(:organisation, :without_dpc)) } + let(:other_user) { create(:user, name: "Danny Rojas", organisation: user.organisation, last_sign_in_at: Time.zone.now) } before do allow(user).to receive(:need_two_factor_authentication?).and_return(false) @@ -1999,7 +2002,7 @@ RSpec.describe UsersController, type: :request do end context "when the user is not part of the same organisation as the current user" do - let(:other_user) { create(:user) } + let(:other_user) { create(:user, organisation: create(:organisation, name: "Another org")) } let(:params) { { id: other_user.id, user: { name: new_name } } } it "updates the user" do @@ -2188,9 +2191,9 @@ RSpec.describe UsersController, type: :request do context "when different organisations manage the logs" do before do - create(:lettings_log, managing_organisation: other_user.organisation, assigned_to: other_user) - create(:lettings_log, managing_organisation: new_organisation_2, assigned_to: other_user) - create(:sales_log, managing_organisation: new_organisation_3, assigned_to: other_user) + create(:lettings_log, owning_organisation: other_user.organisation, managing_organisation: other_user.organisation, assigned_to: other_user) + create(:lettings_log, owning_organisation: other_user.organisation, managing_organisation: new_organisation_2, assigned_to: other_user) + create(:sales_log, owning_organisation: other_user.organisation, managing_organisation: new_organisation_3, assigned_to: other_user) patch "/users/#{other_user.id}/log-reassignment", headers:, params: end @@ -2206,8 +2209,8 @@ RSpec.describe UsersController, type: :request do context "when users organisation manages the logs" do before do - create(:lettings_log, owning_organisation: other_user.organisation, assigned_to: other_user) - create(:sales_log, owning_organisation: other_user.organisation, assigned_to: other_user) + create(:lettings_log, owning_organisation: other_user.organisation, managing_organisation: other_user.organisation, assigned_to: other_user) + create(:sales_log, owning_organisation: other_user.organisation, managing_organisation: other_user.organisation, assigned_to: other_user) patch "/users/#{other_user.id}/log-reassignment", headers:, params: end @@ -2219,15 +2222,15 @@ RSpec.describe UsersController, type: :request do context "when different organisations manage the logs" do before do - create(:lettings_log, owning_organisation: other_user.organisation, assigned_to: other_user) - create(:lettings_log, owning_organisation: new_organisation_2, assigned_to: other_user) + create(:lettings_log, owning_organisation: other_user.organisation, managing_organisation: other_user.organisation, assigned_to: other_user) + create(:lettings_log, owning_organisation: new_organisation_2, managing_organisation: other_user.organisation, assigned_to: other_user) create(:sales_log, owning_organisation: new_organisation_3, managing_organisation: other_user.organisation, assigned_to: other_user) patch "/users/#{other_user.id}/log-reassignment", headers:, params: end it "required the new org to have managing agent relationship with owning organisations" do expect(response).to have_http_status(:unprocessable_entity) - expect(page).to have_content("New org must be a managing agent of #{other_user.organisation_name}, #{new_organisation_2.name}, and #{new_organisation_3.name} to make this change.") + expect(page).to have_content("New org must be a managing agent of #{other_user.organisation.name}, #{new_organisation_2.name}, and #{new_organisation_3.name} to make this change.") end end end diff --git a/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb b/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb index d0e5b3692..b0fcaf8b6 100644 --- a/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb @@ -30,6 +30,29 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do end end + context "when some csv headers are empty (and we don't care about them)" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + context "when parsing csv with headers with extra rows" do before do file.write("Section\n") diff --git a/spec/services/bulk_upload/sales/year2024/csv_parser_spec.rb b/spec/services/bulk_upload/sales/year2024/csv_parser_spec.rb index 9440b7e8c..5f9f003d0 100644 --- a/spec/services/bulk_upload/sales/year2024/csv_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2024/csv_parser_spec.rb @@ -39,6 +39,38 @@ RSpec.describe BulkUpload::Sales::Year2024::CsvParser do end end + context "when some csv headers are empty (and we don't care about them)" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::SalesLogToCsv.new(log:).default_2024_field_numbers_row) + file.write(BulkUpload::SalesLogToCsv.new(log:).to_2024_csv_row) + file.write("\n") + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_22).to eql(log.uprn) + end + + it "counts the number of valid field numbers correctly" do + expect(service).to be_correct_field_count + end + + it "does not parse the last empty row" do + expect(service.row_parsers.count).to eq(1) + end + end + context "when parsing csv with headers in arbitrary order" do let(:seed) { rand } diff --git a/spec/services/csv/lettings_log_csv_service_spec.rb b/spec/services/csv/lettings_log_csv_service_spec.rb index 3134fa451..7a0e15a12 100644 --- a/spec/services/csv/lettings_log_csv_service_spec.rb +++ b/spec/services/csv/lettings_log_csv_service_spec.rb @@ -196,7 +196,7 @@ RSpec.describe Csv::LettingsLogCsvService do describe "the full CSV output" do context "when the requested log year is 2024" do let(:year) { 2024 } - let(:organisation) { create(:organisation, provider_type: "LA") } + let(:organisation) { create(:organisation, provider_type: "LA", name: "MHCLG") } let(:log) do create( :lettings_log, @@ -389,7 +389,7 @@ RSpec.describe Csv::LettingsLogCsvService do context "when the requested log year is 2023" do let(:year) { 2023 } - let(:organisation) { create(:organisation, provider_type: "LA") } + let(:organisation) { create(:organisation, provider_type: "LA", name: "MHCLG") } let(:log) do create( :lettings_log, diff --git a/spec/services/csv/sales_log_csv_service_spec.rb b/spec/services/csv/sales_log_csv_service_spec.rb index 4bbbaf645..3cf56af2e 100644 --- a/spec/services/csv/sales_log_csv_service_spec.rb +++ b/spec/services/csv/sales_log_csv_service_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Csv::SalesLogCsvService do subject(:task) { Rake::Task["data_import:add_variable_definitions"] } let(:form_handler_mock) { instance_double(FormHandler) } - let(:organisation) { create(:organisation) } + let(:organisation) { create(:organisation, name: "MHCLG") } let(:fixed_time) { now } let(:now) { Time.zone.now } let(:user) { create(:user, :support, email: "billyboy@eyeKLAUD.com") } @@ -17,15 +17,23 @@ RSpec.describe Csv::SalesLogCsvService do created_at: fixed_time, updated_at: now, owning_organisation: organisation, + managing_organisation: organisation, purchid: nil, hholdcount: 3, + age1: 30, + sex1: "X", + age2: 35, + sex2: "X", + sex3: "X", age4_known: 1, + sex4: "X", details_known_5: 2, age6_known: nil, age6: nil, ecstat6: nil, relat6: nil, sex6: nil, + town_or_city: "Town or city", address_line1_as_entered: "address line 1 as entered", address_line2_as_entered: "address line 2 as entered", town_or_city_as_entered: "town or city as entered", diff --git a/spec/services/csv/scheme_csv_service_spec.rb b/spec/services/csv/scheme_csv_service_spec.rb index 57f009c65..77c0bae45 100644 --- a/spec/services/csv/scheme_csv_service_spec.rb +++ b/spec/services/csv/scheme_csv_service_spec.rb @@ -1,7 +1,7 @@ require "rails_helper" RSpec.describe Csv::SchemeCsvService do - let(:organisation) { create(:organisation) } + let(:organisation) { create(:organisation, name: "MHCLG") } let(:fixed_time) { Time.zone.local(2023, 6, 26) } let(:scheme) { create(:scheme, :export, owning_organisation: organisation, service_name: "Test name") } let(:location) { create(:location, :export, scheme:) } diff --git a/spec/services/documentation_generator_spec.rb b/spec/services/documentation_generator_spec.rb index 6f4714d01..d214aaa9e 100644 --- a/spec/services/documentation_generator_spec.rb +++ b/spec/services/documentation_generator_spec.rb @@ -127,11 +127,11 @@ describe DocumentationGenerator do context "when the service is run for sales" do let(:log_type) { "sales" } - let(:all_validation_methods) { ["income2_under_soft_min?"] } + let(:all_validation_methods) { ["income2_outside_soft_range_for_ecstat?"] } it "creates new validation documentation records" do expect { described_class.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type) }.to change(LogValidation, :count) - expect(LogValidation.where(validation_name: "income2_under_soft_min?").count).to be_positive + expect(LogValidation.where(validation_name: "income2_outside_soft_range_for_ecstat?").count).to be_positive any_validation = LogValidation.first expect(any_validation.description).to eq("Validates the format.") expect(any_validation.field).not_to be_empty diff --git a/spec/services/exports/lettings_log_export_service_spec.rb b/spec/services/exports/lettings_log_export_service_spec.rb index 6f7d88c91..6a07af8dd 100644 --- a/spec/services/exports/lettings_log_export_service_spec.rb +++ b/spec/services/exports/lettings_log_export_service_spec.rb @@ -15,7 +15,8 @@ RSpec.describe Exports::LettingsLogExportService do let(:expected_data_filename) { "core_2021_2022_apr_mar_f0001_inc0001_pt001.xml" } let(:expected_manifest_filename) { "manifest.xml" } let(:start_time) { Time.zone.local(2022, 5, 1) } - let(:user) { FactoryBot.create(:user, email: "test1@example.com") } + let(:organisation) { create(:organisation, name: "MHCLG", housing_registration_no: 1234) } + let(:user) { FactoryBot.create(:user, email: "test1@example.com", organisation:) } def replace_entity_ids(lettings_log, export_template) export_template.sub!(/\{id\}/, (lettings_log["id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s) @@ -79,7 +80,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and one lettings log is available for export" do - let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) } + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, age1: 35, sex1: "F", age2: 32, sex2: "M", propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", town_or_city: "London", tenancycode: "BZ737", startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) } it "generates a ZIP export file with the expected filename" do expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) @@ -123,7 +124,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and one lettings log with unknown user details is available for export" do - let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, details_known_2: 1, assigned_to: user, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) } + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, details_known_2: 1, assigned_to: user, age1: 35, sex1: "F", propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", town_or_city: "London", tenancycode: "BZ737", startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) } def replace_person_details(export_file) export_file.sub!("32", "-9") @@ -176,7 +177,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and one lettings log is available for export" do - let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, uprn_known: 1, uprn: "100023336956", propcode: "123", postcode_full: "SE2 6RT", ppostcode_full: "SE2 6RT", tenancycode: "BZ737", startdate: Time.zone.local(2023, 4, 2, 10, 36, 49), voiddate: Time.zone.local(2021, 11, 3), mrcdate: Time.zone.local(2022, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) } + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, age1: 35, sex1: "F", age2: 32, sex2: "M", uprn_known: 1, uprn: "100023336956", propcode: "123", postcode_full: "SE2 6RT", ppostcode_full: "SE2 6RT", tenancycode: "BZ737", startdate: Time.zone.local(2023, 4, 2, 10, 36, 49), voiddate: Time.zone.local(2021, 11, 3), mrcdate: Time.zone.local(2022, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) } let(:expected_zip_filename) { "core_2023_2024_apr_mar_f0001_inc0001.zip" } let(:expected_data_filename) { "core_2023_2024_apr_mar_f0001_inc0001_pt001.xml" } let(:xml_export_file) { File.open("spec/fixtures/exports/general_needs_log_23_24.xml", "r:UTF-8") } @@ -396,7 +397,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and one lettings log with duplicate reference is available for export" do - let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4, duplicate_set_id: 123) } + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, age1: 35, sex1: "F", age2: 32, sex2: "M", propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", town_or_city: "London", tenancycode: "BZ737", startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4, duplicate_set_id: 123) } def replace_duplicate_set_id(export_file) export_file.sub!("", "123") @@ -429,7 +430,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and one lettings log is available for export" do - let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, ppostcode_full: "A1 1AA", nationality_all_group: 13, propcode: "123", postcode_full: "SE2 6RT", tenancycode: "BZ737", startdate: Time.zone.local(2024, 4, 2, 10, 36, 49), voiddate: Time.zone.local(2021, 11, 3), mrcdate: Time.zone.local(2022, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4, creation_method: 2, bulk_upload_id: 1, address_line1_as_entered: "address line 1 as entered", address_line2_as_entered: "address line 2 as entered", town_or_city_as_entered: "town or city as entered", county_as_entered: "county as entered", postcode_full_as_entered: "AB1 2CD", la_as_entered: "la as entered") } + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, age1: 35, sex1: "F", age2: 32, sex2: "M", ppostcode_full: "A1 1AA", nationality_all_group: 13, propcode: "123", postcode_full: "SE2 6RT", tenancycode: "BZ737", startdate: Time.zone.local(2024, 4, 2, 10, 36, 49), voiddate: Time.zone.local(2021, 11, 3), mrcdate: Time.zone.local(2022, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4, creation_method: 2, bulk_upload_id: 1, address_line1_as_entered: "address line 1 as entered", address_line2_as_entered: "address line 2 as entered", town_or_city_as_entered: "town or city as entered", county_as_entered: "county as entered", postcode_full_as_entered: "AB1 2CD", la_as_entered: "la as entered") } let(:expected_zip_filename) { "core_2024_2025_apr_mar_f0001_inc0001.zip" } let(:expected_data_filename) { "core_2024_2025_apr_mar_f0001_inc0001_pt001.xml" } let(:xml_export_file) { File.open("spec/fixtures/exports/general_needs_log_24_25.xml", "r:UTF-8") } @@ -450,13 +451,13 @@ RSpec.describe Exports::LettingsLogExportService do context "when exporting a supported housing lettings logs in XML" do let(:export_file) { File.open("spec/fixtures/exports/supported_housing_logs.xml", "r:UTF-8") } - let(:organisation) { FactoryBot.create(:organisation, provider_type: "LA") } + let(:organisation) { FactoryBot.create(:organisation, name: "MHCLG", provider_type: "LA", housing_registration_no: 1234) } let(:user) { FactoryBot.create(:user, organisation:, email: "fake@email.com") } let(:other_user) { FactoryBot.create(:user, organisation:, email: "other@email.com") } let(:scheme) { FactoryBot.create(:scheme, :export, owning_organisation: organisation) } let(:location) { FactoryBot.create(:location, :export, scheme:, startdate: Time.zone.local(2021, 4, 1), old_id: "1a") } - let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, :export, :sh, scheme:, location:, assigned_to: user, updated_by: other_user, owning_organisation: organisation, startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), underoccupation_benefitcap: 4, sheltered: 1) } + let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, :export, :sh, scheme:, location:, assigned_to: user, updated_by: other_user, owning_organisation: organisation, age1: 35, sex1: "F", age2: 32, sex2: "M", startdate: Time.zone.local(2022, 2, 2, 10, 36, 49), voiddate: Time.zone.local(2019, 11, 3), mrcdate: Time.zone.local(2020, 5, 5, 10, 36, 49), underoccupation_benefitcap: 4, sheltered: 1) } before do lettings_log.postcode_full = nil diff --git a/spec/services/exports/organisation_export_service_spec.rb b/spec/services/exports/organisation_export_service_spec.rb index 4de0e84a8..43ca19095 100644 --- a/spec/services/exports/organisation_export_service_spec.rb +++ b/spec/services/exports/organisation_export_service_spec.rb @@ -42,7 +42,7 @@ RSpec.describe Exports::OrganisationExportService do end context "and one organisation is available for export" do - let!(:organisation) { create(:organisation) } + let!(:organisation) { create(:organisation, name: "MHCLG", address_line1: "2 Marsham Street", address_line2: "London", postcode: "SW1P 4DF", housing_registration_no: "1234") } it "generates a ZIP export file with the expected filename" do expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) diff --git a/spec/services/exports/user_export_service_spec.rb b/spec/services/exports/user_export_service_spec.rb index 713d6f907..8a0e22267 100644 --- a/spec/services/exports/user_export_service_spec.rb +++ b/spec/services/exports/user_export_service_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Exports::UserExportService do let(:expected_data_filename) { "users_2024_2025_apr_mar_f0001_inc0001_pt001.xml" } let(:expected_manifest_filename) { "manifest.xml" } let(:start_time) { Time.zone.local(2022, 5, 1) } - let(:organisation) { create(:organisation, with_dsa: false) } + let(:organisation) { create(:organisation, name: "MHCLG", with_dsa: false) } def replace_entity_ids(user, export_template) export_template.sub!(/\{id\}/, user["id"].to_s) @@ -42,7 +42,7 @@ RSpec.describe Exports::UserExportService do end context "and one user is available for export" do - let!(:user) { create(:user, organisation:, phone_extension: "123") } + let!(:user) { create(:user, organisation:, name: "Danny Rojas", phone_extension: "123") } it "generates a ZIP export file with the expected filename" do expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) diff --git a/spec/services/mandatory_collection_resources_service_spec.rb b/spec/services/mandatory_collection_resources_service_spec.rb index 423370e96..6b67e1d13 100644 --- a/spec/services/mandatory_collection_resources_service_spec.rb +++ b/spec/services/mandatory_collection_resources_service_spec.rb @@ -17,7 +17,7 @@ describe MandatoryCollectionResourcesService do it "returns a CollectionResource object with the correct attributes" do resource = service.generate_resource("lettings", 2024, "paper_form") expect(resource.resource_type).to eq("paper_form") - expect(resource.display_name).to eq("lettings log for tenants (2024 to 2025)") + expect(resource.display_name).to eq("lettings paper form (2024 to 2025)") expect(resource.short_display_name).to eq("Paper form") expect(resource.year).to eq(2024) expect(resource.log_type).to eq("lettings") diff --git a/spec/services/merge/merge_organisations_service_spec.rb b/spec/services/merge/merge_organisations_service_spec.rb index 7163a142f..fbb52c2a5 100644 --- a/spec/services/merge/merge_organisations_service_spec.rb +++ b/spec/services/merge/merge_organisations_service_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Merge::MergeOrganisationsService do it "moves the users from merging organisation to absorbing organisation" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") merge_organisations_service.call @@ -477,7 +477,7 @@ RSpec.describe Merge::MergeOrganisationsService do it "logs the merged schemes and locations" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") expect(Rails.logger).to receive(:info).with(/\t#{scheme.service_name} \(S/) @@ -623,7 +623,7 @@ RSpec.describe Merge::MergeOrganisationsService do context "and merging sales logs" do let(:owning_organisation) { create(:organisation, holds_own_stock: true) } - let!(:sales_log) { create(:sales_log, saledate: Time.zone.today, owning_organisation: merging_organisation, purchid: "owned") } + let!(:sales_log) { create(:sales_log, saledate: Time.zone.today, owning_organisation: merging_organisation, managing_organisation: merging_organisation, purchid: "owned") } let!(:managed_sales_log) { create(:sales_log, saledate: Time.zone.today, purchid: "managed") } before do @@ -744,7 +744,7 @@ RSpec.describe Merge::MergeOrganisationsService do it "logs the merged schemes" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") expect(Rails.logger).to receive(:info).with(/\t#{scheme.service_name} \(S/) @@ -922,7 +922,7 @@ RSpec.describe Merge::MergeOrganisationsService do let!(:merging_organisation_user) { create(:user, organisation: merging_organisation, name: "fake name", email: "fake@email.com") } before do - create_list(:user, 5, organisation: merging_organisation_too) + create_list(:user, 5, organisation: merging_organisation_too, name: "Danny Rojas") end it "sets merge date and absorbing organisation on merged organisations" do @@ -961,10 +961,11 @@ RSpec.describe Merge::MergeOrganisationsService do context "and merging users" do it "moves the users from merging organisations to absorbing organisation" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("Merged users from second org:") - expect(Rails.logger).to receive(:info).with(/\tDanny Rojas/).exactly(6).times + expect(Rails.logger).to receive(:info).with(/\tDanny Rojas/).exactly(5).times + expect(Rails.logger).to receive(:info).with(/\t#{merging_organisation_too.data_protection_officers.first.name}/) expect(Rails.logger).to receive(:info).with("New schemes from fake org:") expect(Rails.logger).to receive(:info).with("New schemes from second org:") merge_organisations_service.call @@ -1113,7 +1114,7 @@ RSpec.describe Merge::MergeOrganisationsService do it "moves the users from merging organisation to absorbing organisation" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") merge_organisations_service.call @@ -1251,7 +1252,7 @@ RSpec.describe Merge::MergeOrganisationsService do it "logs the merged schemes" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") expect(Rails.logger).to receive(:info).with(/\t#{scheme.service_name} \(S/) @@ -1462,7 +1463,7 @@ RSpec.describe Merge::MergeOrganisationsService do it "logs the merged schemes" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") expect(Rails.logger).to receive(:info).with(/\t#{scheme.service_name} \(S/) @@ -1590,15 +1591,16 @@ RSpec.describe Merge::MergeOrganisationsService do let!(:merging_organisation_user) { create(:user, organisation: merging_organisation, name: "fake name", email: "fake@email.com") } before do - create_list(:user, 5, organisation: merging_organisation_too) + create_list(:user, 5, organisation: merging_organisation_too, name: "Danny Rojas") end it "moves the users from merging organisations to absorbing organisation" do expect(Rails.logger).to receive(:info).with("Merged users from fake org:") - expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{merging_organisation.data_protection_officers.first.email})") + expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("Merged users from second org:") - expect(Rails.logger).to receive(:info).with(/\tDanny Rojas/).exactly(6).times + expect(Rails.logger).to receive(:info).with(/\tDanny Rojas/).exactly(5).times + expect(Rails.logger).to receive(:info).with(/\t#{merging_organisation_too.data_protection_officers.first.name}/) expect(Rails.logger).to receive(:info).with("New schemes from fake org:") expect(Rails.logger).to receive(:info).with("New schemes from second org:") merge_organisations_service.call