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/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..a6e2c08e5 100644
--- a/app/controllers/merge_requests_controller.rb
+++ b/app/controllers/merge_requests_controller.rb
@@ -143,6 +143,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
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..28c693935 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -276,4 +276,8 @@ 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
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/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb
index ae1da086d..ced530b17 100644
--- a/app/models/derived_variables/lettings_log_variables.rb
+++ b/app/models/derived_variables/lettings_log_variables.rb
@@ -133,6 +133,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 3de306584..86fef99e6 100644
--- a/app/models/derived_variables/sales_log_variables.rb
+++ b/app/models/derived_variables/sales_log_variables.rb
@@ -84,6 +84,11 @@ 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
+
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/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/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/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/sections/sale_information.rb b/app/models/form/sales/sections/sale_information.rb
index b57eb70a6..22dbbef5a 100644
--- a/app/models/form/sales/sections/sale_information.rb
+++ b/app/models/form/sales/sections/sale_information.rb
@@ -5,9 +5,17 @@ class Form::Sales::Sections::SaleInformation < ::Form::Section
@label = "Sale information"
@description = ""
@subsections = [
- Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self),
+ shared_ownership_scheme_subsection,
Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self),
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)
+ else
+ Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self)
+ end
+ end
end
diff --git a/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb b/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb
new file mode 100644
index 000000000..5dfb322a2
--- /dev/null
+++ b/app/models/form/sales/subsections/shared_ownership_initial_purchase.rb
@@ -0,0 +1,48 @@
+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 }]
+ 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(nil, 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::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..20a088eae 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),
diff --git a/app/models/location.rb b/app/models/location.rb
index 03af24a94..12c6f2fad 100644
--- a/app/models/location.rb
+++ b/app/models/location.rb
@@ -54,13 +54,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|
@@ -206,7 +206,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/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/scheme.rb b/app/models/scheme.rb
index 2c73acc06..33f236374 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))
}
@@ -314,7 +314,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/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/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/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/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/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/locales/en.yml b/config/locales/en.yml
index f8bb8255b..698618717 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -183,8 +183,11 @@ 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."
notification:
attributes:
title:
@@ -370,6 +373,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/2025/sales/sale_information.en.yml b/config/locales/forms/2025/sales/sale_information.en.yml
index 33826e58b..9a273d1c3 100644
--- a/config/locales/forms/2025/sales/sale_information.en.yml
+++ b/config/locales/forms/2025/sales/sale_information.en.yml
@@ -107,9 +107,9 @@ en:
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?"
+ 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?"
mortgageused:
page_header: "Mortgage Amount"
@@ -168,9 +168,9 @@ en:
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 +199,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/routes.rb b/config/routes.rb
index 1c7af8c59..55d58b41b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -389,6 +389,11 @@ Rails.application.routes.draw do
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/schema.rb b/db/schema.rb
index 5b6ddacdb..9aa744dc2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -758,6 +758,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_18_104046) 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.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..9654b2b78 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -15,6 +15,8 @@ def create_data_protection_confirmation(user)
signed_at: Time.zone.local(2019, 1, 1),
data_protection_officer_email: user.email,
data_protection_officer_name: user.name,
+ organisation_name: user.organisation.name,
+ organisation_address: user.organisation.address_row,
)
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/schemes_spec.rb b/spec/features/schemes_spec.rb
index 33ab00b34..7c7d9f3fb 100644
--- a/spec/features/schemes_spec.rb
+++ b/spec/features/schemes_spec.rb
@@ -677,7 +677,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/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/sections/sale_information_spec.rb b/spec/models/form/sales/sections/sale_information_spec.rb
index 0b2ab4144..8167c92a3 100644
--- a/spec/models/form/sales/sections/sale_information_spec.rb
+++ b/spec/models/form/sales/sections/sale_information_spec.rb
@@ -5,18 +5,32 @@ 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) }
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, start_year_2025_or_later?: true) }
+
+ it "has correct subsections" do
+ expect(sale_information.subsections.map(&:id)).to eq(%w[
+ shared_ownership_initial_purchase
+ discounted_ownership_scheme
+ outright_sale
+ ])
+ end
end
it "has the correct id" do
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..3b2d72b01
--- /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
+ 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/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..18581fb6e 100644
--- a/spec/models/location_spec.rb
+++ b/spec/models/location_spec.rb
@@ -929,6 +929,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/sales_log_spec.rb b/spec/models/sales_log_spec.rb
index ae9b00d4c..9fe5a02a9 100644
--- a/spec/models/sales_log_spec.rb
+++ b/spec/models/sales_log_spec.rb
@@ -978,5 +978,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/requests/merge_requests_controller_spec.rb b/spec/requests/merge_requests_controller_spec.rb
index dc1dd817d..a73db8067 100644
--- a/spec/requests/merge_requests_controller_spec.rb
+++ b/spec/requests/merge_requests_controller_spec.rb
@@ -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: [] } } }
@@ -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
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..2b7210402 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
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 }