Browse Source

Merge branch 'main' into cldc-2812-run-app-from-relative-url

cldc-2812-run-app-from-relative-url
Rachael Booth 2 years ago
parent
commit
d0decfcc95
  1. 220
      .github/workflows/paas_only_production_pipeline.yml
  2. 14
      .github/workflows/production_pipeline.yml
  3. 3
      app/components/search_component.html.erb
  4. 10
      app/components/search_result_caption_component.html.erb
  5. 6
      app/components/search_result_caption_component.rb
  6. 9
      app/controllers/application_controller.rb
  7. 4
      app/controllers/delete_logs_controller.rb
  8. 37
      app/controllers/duplicate_logs_controller.rb
  9. 6
      app/controllers/form_controller.rb
  10. 7
      app/controllers/maintenance_controller.rb
  11. 2
      app/controllers/sessions_controller.rb
  12. 29
      app/controllers/users_controller.rb
  13. 53
      app/helpers/duplicate_logs_helper.rb
  14. 35
      app/helpers/filters_helper.rb
  15. 8
      app/helpers/form_page_helper.rb
  16. 13
      app/models/derived_variables/lettings_log_variables.rb
  17. 3
      app/models/form/lettings/pages/address.rb
  18. 1
      app/models/form/lettings/pages/location.rb
  19. 2
      app/models/form/lettings/pages/max_rent_value_check.rb
  20. 2
      app/models/form/lettings/pages/min_rent_value_check.rb
  21. 1
      app/models/form/lettings/questions/location_id.rb
  22. 4
      app/models/form/lettings/questions/managing_organisation.rb
  23. 1
      app/models/form/lettings/questions/referral.rb
  24. 1
      app/models/form/lettings/questions/referral_prp.rb
  25. 1
      app/models/form/lettings/questions/referral_supported_housing.rb
  26. 2
      app/models/form/lettings/questions/referral_supported_housing_prp.rb
  27. 38
      app/models/form/lettings/questions/stock_owner.rb
  28. 4
      app/models/form/sales/pages/address.rb
  29. 4
      app/models/form/sales/pages/uprn.rb
  30. 4
      app/models/form/sales/questions/owning_organisation_id.rb
  31. 23
      app/models/lettings_log.rb
  32. 10
      app/models/location.rb
  33. 7
      app/models/log.rb
  34. 8
      app/models/organisation.rb
  35. 11
      app/models/sales_log.rb
  36. 23
      app/models/scheme.rb
  37. 6
      app/models/user.rb
  38. 20
      app/models/validations/financial_validations.rb
  39. 3
      app/models/validations/household_validations.rb
  40. 8
      app/models/validations/sales/setup_validations.rb
  41. 20
      app/models/validations/setup_validations.rb
  42. 2
      app/models/validations/shared_validations.rb
  43. 45
      app/services/bulk_upload/lettings/year2022/row_parser.rb
  44. 47
      app/services/bulk_upload/lettings/year2023/row_parser.rb
  45. 2
      app/services/bulk_upload/sales/year2022/row_parser.rb
  46. 2
      app/services/bulk_upload/sales/year2023/row_parser.rb
  47. 6
      app/services/csv/sales_log_csv_service.rb
  48. 4
      app/services/feature_toggle.rb
  49. 26
      app/services/merge/merge_organisations_service.rb
  50. 14
      app/views/duplicate_logs/_duplicate_log_check_answers.erb
  51. 4
      app/views/duplicate_logs/index.html.erb
  52. 11
      app/views/duplicate_logs/no_more_duplicates.html.erb
  53. 4
      app/views/form/guidance/_finding_location.erb
  54. 16
      app/views/form/page.html.erb
  55. 12
      app/views/layouts/application.html.erb
  56. 6
      app/views/locations/_location_filters.html.erb
  57. 2
      app/views/locations/index.html.erb
  58. 54
      app/views/logs/_log_filters.html.erb
  59. 29
      app/views/logs/_log_list.html.erb
  60. 6
      app/views/logs/delete_duplicates.html.erb
  61. 2
      app/views/logs/download_csv.html.erb
  62. 2
      app/views/logs/index.html.erb
  63. 11
      app/views/maintenance/service_unavailable.html.erb
  64. 2
      app/views/organisation_relationships/_managing_agent_list.erb
  65. 2
      app/views/organisation_relationships/_stock_owner_list.erb
  66. 34
      app/views/organisations/_merged_organisation_details.html.erb
  67. 2
      app/views/organisations/_organisation_list.html.erb
  68. 2
      app/views/organisations/logs.html.erb
  69. 1
      app/views/organisations/show.html.erb
  70. 6
      app/views/schemes/_scheme_filters.html.erb
  71. 2
      app/views/schemes/_scheme_list.html.erb
  72. 2
      app/views/schemes/support.html.erb
  73. 7
      app/views/users/_user_filters.html.erb
  74. 2
      app/views/users/_user_list.html.erb
  75. 6
      app/views/users/new.html.erb
  76. 12
      config/forms/2021_2022.json
  77. 20
      config/forms/2022_2023.json
  78. 5
      config/initializers/sidekiq.rb
  79. 26
      config/locales/en.yml
  80. 1
      config/routes.rb
  81. 5
      db/migrate/20231023142854_add_available_from_to_org.rb
  82. 3
      db/schema.rb
  83. 2
      docs/adr/index.md
  84. 15
      docs/app_api.md
  85. 2
      docs/documentation_website.md
  86. 2
      docs/form/index.md
  87. 2
      docs/setup.md
  88. 4
      lib/tasks/correct_illness_from_csv.rake
  89. 22
      lib/tasks/import_address_from_csv.rake
  90. 33
      lib/tasks/merge_organisations.rake
  91. 18
      lib/tasks/recalculate_irproduct_values.rake
  92. 13
      lib/tasks/recalculate_lar_values.rake
  93. 35
      lib/tasks/squish_names.rake
  94. 47
      spec/components/search_result_caption_component_spec.rb
  95. 27
      spec/controllers/maintenance_controller_spec.rb
  96. 5
      spec/factories/lettings_log.rb
  97. 1
      spec/factories/user.rb
  98. 14
      spec/features/form/form_navigation_spec.rb
  99. 18
      spec/features/lettings_log_spec.rb
  100. 30
      spec/features/organisation_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

220
.github/workflows/paas_only_production_pipeline.yml

@ -0,0 +1,220 @@
name: PaaS-only Production CI/CD Pipeline
on:
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 18
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec\/(?!features)']
feature_test:
name: Feature Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 18
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features --fail-fast
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 18
- name: Install packages and symlink local dependencies
run: |
yarn install --immutable --immutable-cache --check-cache
- name: Lint
run: |
bundle exec rake lint
audit:
name: Audit dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Audit
run: |
bundle exec bundler-audit
deploy:
name: Deploy
concurrency: "production"
runs-on: ubuntu-latest
environment: "production"
needs: [lint, test, feature_test, audit]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Cloud Foundry CLI
run: |
wget --user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo apt-key add -
echo "deb https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list
sudo apt-get update
sudo apt-get install cf8-cli
- name: Deploy
env:
CF_USERNAME: ${{ secrets.CF_USERNAME }}
CF_PASSWORD: ${{ secrets.CF_PASSWORD }}
CF_API_ENDPOINT: ${{ secrets.CF_API_ENDPOINT }}
CF_SPACE: ${{ secrets.CF_SPACE }}
CF_ORG: ${{ secrets.CF_ORG }}
APP_NAME: dluhc-core-production
GOVUK_NOTIFY_API_KEY: ${{ secrets.GOVUK_NOTIFY_API_KEY }}
APP_HOST: ${{ secrets.APP_HOST }}
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
OS_DATA_KEY: ${{ secrets.OS_DATA_KEY }}
IMPORT_PAAS_INSTANCE: ${{ secrets.IMPORT_PAAS_INSTANCE }}
EXPORT_PAAS_INSTANCE: ${{ secrets.EXPORT_PAAS_INSTANCE }}
S3_CONFIG: ${{ secrets.S3_CONFIG }}
CSV_DOWNLOAD_PAAS_INSTANCE: ${{ secrets.CSV_DOWNLOAD_PAAS_INSTANCE }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: |
cf api $CF_API_ENDPOINT
cf auth
cf target -o $CF_ORG -s $CF_SPACE
cf set-env $APP_NAME GOVUK_NOTIFY_API_KEY $GOVUK_NOTIFY_API_KEY
cf set-env $APP_NAME APP_HOST $APP_HOST
cf set-env $APP_NAME RAILS_MASTER_KEY $RAILS_MASTER_KEY
cf set-env $APP_NAME OS_DATA_KEY $OS_DATA_KEY
cf set-env $APP_NAME IMPORT_PAAS_INSTANCE $IMPORT_PAAS_INSTANCE
cf set-env $APP_NAME EXPORT_PAAS_INSTANCE $EXPORT_PAAS_INSTANCE
cf set-env $APP_NAME S3_CONFIG $S3_CONFIG
cf set-env $APP_NAME CSV_DOWNLOAD_PAAS_INSTANCE $CSV_DOWNLOAD_PAAS_INSTANCE
cf set-env $APP_NAME SENTRY_DSN $SENTRY_DSN
cf push $APP_NAME --strategy rolling

14
.github/workflows/production_pipeline.yml

@ -167,9 +167,19 @@ jobs:
with: with:
bundler-cache: true bundler-cache: true
- name: Rubocop - name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 18
- name: Install packages and symlink local dependencies
run: |
yarn install --immutable --immutable-cache --check-cache
- name: Lint
run: | run: |
bundle exec rubocop bundle exec rake lint
audit: audit:
name: Audit dependencies name: Audit dependencies

3
app/components/search_component.html.erb

@ -10,6 +10,7 @@
autocomplete: "off", autocomplete: "off",
class: "app-search__input" %> class: "app-search__input" %>
<%= f.govuk_submit "Search", classes: "app-search__button govuk-button--secondary" %> <%= f.govuk_submit "Search", classes: "app-search__button" %>
<%= govuk_button_link_to "Clear search", path(current_user), secondary: true, class: "app-search__button" %>
</div> </div>
<% end %> <% end %>

10
app/components/search_result_caption_component.html.erb

@ -1,7 +1,11 @@
<span class="govuk-!-margin-right-4"> <span class="govuk-!-margin-right-4">
<% if searched.present? %> <% if searched.present? && filters_count&.positive? %>
<strong><%= count %></strong> <%= item_label %> found matching ‘<%= searched %>’ of <strong><%= total_count %></strong> total <%= item %>. <%= govuk_link_to("Clear search", path) %> <strong><%= count %></strong> <%= item_label.pluralize(count) %> matching search and filters<br>
<% elsif searched.present? %>
<strong><%= count %></strong> <%= item_label.pluralize(count) %> matching search<br>
<% elsif filters_count&.positive? %>
<strong><%= count %></strong> <%= item_label.pluralize(count) %> matching filters<br>
<% else %> <% else %>
<strong><%= count %></strong> total <%= item %> <strong><%= count %></strong> matching <%= item %>
<% end %> <% end %>
</span> </span>

6
app/components/search_result_caption_component.rb

@ -1,13 +1,13 @@
class SearchResultCaptionComponent < ViewComponent::Base class SearchResultCaptionComponent < ViewComponent::Base
attr_reader :searched, :count, :item_label, :total_count, :item, :path attr_reader :searched, :count, :item_label, :total_count, :item, :filters_count
def initialize(searched:, count:, item_label:, total_count:, item:, path:) def initialize(searched:, count:, item_label:, total_count:, item:, filters_count:)
@searched = searched @searched = searched
@count = count @count = count
@item_label = item_label @item_label = item_label
@total_count = total_count @total_count = total_count
@item = item @item = item
@path = path @filters_count = filters_count
super super
end end
end end

9
app/controllers/application_controller.rb

@ -3,8 +3,17 @@ class ApplicationController < ActionController::Base
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
before_action :check_maintenance
before_action :set_paper_trail_whodunnit before_action :set_paper_trail_whodunnit
def check_maintenance
if FeatureToggle.maintenance_mode_enabled? && !%w[service-unavailable accessibility-statement privacy-notice cookies].include?(request.fullpath.split("?")[0].delete("/"))
redirect_to service_unavailable_path
elsif !FeatureToggle.maintenance_mode_enabled? && request.fullpath.split("?")[0].delete("/") == "service-unavailable"
redirect_back(fallback_location: root_path)
end
end
def render_not_found def render_not_found
render "errors/not_found", status: :not_found render "errors/not_found", status: :not_found
end end

4
app/controllers/delete_logs_controller.rb

@ -27,7 +27,7 @@ class DeleteLogsController < ApplicationController
logs = LettingsLog.find(params.require(:ids)) logs = LettingsLog.find(params.require(:ids))
discard logs discard logs
if request.referer&.include?("delete-duplicates") if request.referer&.include?("delete-duplicates")
redirect_to lettings_log_duplicate_logs_path(lettings_log_id: params["remaining_log_id"], original_log_id: params["original_log_id"]), notice: I18n.t("notification.duplicate_logs_deleted", count: logs.count, log_ids: duplicate_log_ids(logs)) redirect_to lettings_log_duplicate_logs_path(lettings_log_id: params["remaining_log_id"], original_log_id: params["original_log_id"], referrer: params[:referrer], organisation_id: params[:organisation_id]), notice: I18n.t("notification.duplicate_logs_deleted", count: logs.count, log_ids: duplicate_log_ids(logs))
else else
redirect_to lettings_logs_path, notice: I18n.t("notification.logs_deleted", count: logs.count) redirect_to lettings_logs_path, notice: I18n.t("notification.logs_deleted", count: logs.count)
end end
@ -56,7 +56,7 @@ class DeleteLogsController < ApplicationController
logs = SalesLog.find(params.require(:ids)) logs = SalesLog.find(params.require(:ids))
discard logs discard logs
if request.referer&.include?("delete-duplicates") if request.referer&.include?("delete-duplicates")
redirect_to sales_log_duplicate_logs_path(sales_log_id: params["remaining_log_id"], original_log_id: params["original_log_id"]), notice: I18n.t("notification.duplicate_logs_deleted", count: logs.count, log_ids: duplicate_log_ids(logs)) redirect_to sales_log_duplicate_logs_path(sales_log_id: params["remaining_log_id"], original_log_id: params["original_log_id"], referrer: params[:referrer], organisation_id: params[:organisation_id]), notice: I18n.t("notification.duplicate_logs_deleted", count: logs.count, log_ids: duplicate_log_ids(logs))
else else
redirect_to sales_logs_path, notice: I18n.t("notification.logs_deleted", count: logs.count) redirect_to sales_logs_path, notice: I18n.t("notification.logs_deleted", count: logs.count)
end end

37
app/controllers/duplicate_logs_controller.rb

@ -5,15 +5,12 @@ class DuplicateLogsController < ApplicationController
before_action :find_resource_by_named_id before_action :find_resource_by_named_id
before_action :find_duplicates_for_a_log before_action :find_duplicates_for_a_log
before_action :find_original_log before_action :find_original_log
before_action :find_organisation, only: [:index]
before_action :find_all_duplicates, only: [:index] before_action :find_all_duplicates, only: [:index]
def show def show
if @log if @log
@all_duplicates = [@log, *@duplicate_logs] @all_duplicates = [@log, *@duplicate_logs]
@duplicate_check_questions = duplicate_check_question_ids.map { |question_id|
question = @log.form.get_question(question_id, @log)
question if question.page.routed_to?(@log, current_user)
}.compact
else else
render_not_found render_not_found
end end
@ -26,10 +23,8 @@ class DuplicateLogsController < ApplicationController
end end
def index def index
render_not_found if @duplicates.blank?
@duplicate_sets_count = @duplicates[:lettings].count + @duplicates[:sales].count @duplicate_sets_count = @duplicates[:lettings].count + @duplicates[:sales].count
render_not_found if @duplicate_sets_count.zero? render "duplicate_logs/no_more_duplicates" if @duplicate_sets_count.zero?
end end
private private
@ -55,29 +50,9 @@ private
def find_all_duplicates def find_all_duplicates
return @duplicates = duplicates_for_user(current_user) if current_user.data_provider? return @duplicates = duplicates_for_user(current_user) if current_user.data_provider?
organisation = current_user.support? ? Organisation.find(params[:organisation_id]) : current_user.organisation return unless @organisation
return unless organisation
@duplicates = duplicates_for_organisation(organisation) @duplicates = duplicates_for_organisation(@organisation)
end
def duplicate_check_question_ids
if @log.lettings?
["owning_organisation_id",
"startdate",
"tenancycode",
"postcode_full",
"scheme_id",
"location_id",
"age1",
"sex1",
"ecstat1",
@log.household_charge == 1 ? "household_charge" : nil,
"tcharge",
@log.is_carehome? ? "chcharge" : nil].compact
else
%w[owning_organisation_id saledate purchid age1 sex1 ecstat1 postcode_full]
end
end end
def find_original_log def find_original_log
@ -89,4 +64,8 @@ private
current_user.lettings_logs.find_by(id: original_log_id) current_user.lettings_logs.find_by(id: original_log_id)
end end
end end
def find_organisation
@organisation = current_user.support? ? Organisation.find(params[:organisation_id]) : current_user.organisation
end
end end

6
app/controllers/form_controller.rb

@ -165,7 +165,7 @@ private
def successful_redirect_path def successful_redirect_path
if FeatureToggle.deduplication_flow_enabled? if FeatureToggle.deduplication_flow_enabled?
if is_referrer_type?("duplicate_logs") if is_referrer_type?("duplicate_logs") || is_referrer_type?("duplicate_logs_banner")
return correcting_duplicate_logs_redirect_path return correcting_duplicate_logs_redirect_path
end end
@ -251,10 +251,10 @@ private
if original_log.present? && current_user.send(class_name.pluralize).duplicate_logs(original_log).count.positive? if original_log.present? && current_user.send(class_name.pluralize).duplicate_logs(original_log).count.positive?
flash[:notice] = deduplication_success_banner unless current_user.send(class_name.pluralize).duplicate_logs(@log).count.positive? flash[:notice] = deduplication_success_banner unless current_user.send(class_name.pluralize).duplicate_logs(@log).count.positive?
send("#{class_name}_duplicate_logs_path", original_log, original_log_id: original_log.id) send("#{class_name}_duplicate_logs_path", original_log, original_log_id: original_log.id, referrer: params[:referrer], organisation_id: params[:organisation_id])
else else
flash[:notice] = deduplication_success_banner flash[:notice] = deduplication_success_banner
send("#{class_name}_duplicate_logs_path", "#{class_name}_id".to_sym => from_referrer_query("first_remaining_duplicate_id"), original_log_id: from_referrer_query("original_log_id")) send("#{class_name}_duplicate_logs_path", "#{class_name}_id".to_sym => from_referrer_query("first_remaining_duplicate_id"), original_log_id: from_referrer_query("original_log_id"), referrer: params[:referrer], organisation_id: params[:organisation_id])
end end
end end

7
app/controllers/maintenance_controller.rb

@ -0,0 +1,7 @@
class MaintenanceController < ApplicationController
def service_unavailable
if current_user
sign_out
end
end
end

2
app/controllers/sessions_controller.rb

@ -3,7 +3,7 @@ class SessionsController < ApplicationController
session[session_name_for(params[:filter_type])] = "{}" session[session_name_for(params[:filter_type])] = "{}"
path_params = params[:path_params].presence || {} path_params = params[:path_params].presence || {}
redirect_to send("#{params[:filter_type]}_path", scheme_id: path_params[:scheme_id]) redirect_to send("#{params[:filter_type]}_path", scheme_id: path_params[:scheme_id], search: path_params[:search])
end end
private private

29
app/controllers/users_controller.rb

@ -5,7 +5,7 @@ class UsersController < ApplicationController
include Modules::SearchFilter include Modules::SearchFilter
before_action :authenticate_user! before_action :authenticate_user!
before_action :find_resource, except: %i[new create] before_action :find_user, except: %i[new create]
before_action :authenticate_scope!, except: %i[new] before_action :authenticate_scope!, except: %i[new]
before_action :session_filters, if: :current_user, only: %i[index] before_action :session_filters, if: :current_user, only: %i[index]
before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index] before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index]
@ -49,7 +49,8 @@ class UsersController < ApplicationController
end end
def update def update
if @user.update(user_params) validate_attributes
if @user.errors.empty? && @user.update(user_params)
if @user == current_user if @user == current_user
bypass_sign_in @user bypass_sign_in @user
flash[:notice] = I18n.t("devise.passwords.updated") if user_params.key?("password") flash[:notice] = I18n.t("devise.passwords.updated") if user_params.key?("password")
@ -83,18 +84,18 @@ class UsersController < ApplicationController
def new def new
@organisation_id = params["organisation_id"] @organisation_id = params["organisation_id"]
@resource = User.new @user = User.new
end end
def create def create
@resource = User.new(user_params.merge(org_params).merge(password_params)) @user = User.new(user_params.merge(org_params).merge(password_params))
validate_attributes validate_attributes
if @resource.errors.empty? && @resource.save if @user.errors.empty? && @user.save
redirect_to created_user_redirect_path redirect_to created_user_redirect_path
else else
unless @resource.errors[:organisation].empty? unless @user.errors[:organisation].empty?
@resource.errors.delete(:organisation) @user.errors.delete(:organisation)
end end
render :new, status: :unprocessable_entity render :new, status: :unprocessable_entity
end end
@ -124,15 +125,15 @@ class UsersController < ApplicationController
private private
def validate_attributes def validate_attributes
@resource.validate @user.validate
if user_params[:role].present? && !current_user.assignable_roles.key?(user_params[:role].to_sym) if user_params[:role].present? && !current_user.assignable_roles.key?(user_params[:role].to_sym)
@resource.errors.add :role, I18n.t("validations.role.invalid") @user.errors.add :role, I18n.t("validations.role.invalid")
end end
if user_params[:phone].blank? if !user_params[:phone].nil? && user_params[:phone].blank?
@resource.errors.add :phone, :blank @user.errors.add :phone, :blank
elsif !valid_phone_number?(user_params[:phone]) elsif !user_params[:phone].nil? && !valid_phone_number?(user_params[:phone])
@resource.errors.add :phone @user.errors.add :phone
end end
end end
@ -188,7 +189,7 @@ private
end end
end end
def find_resource def find_user
@user = User.find_by(id: params[:user_id]) || User.find_by(id: params[:id]) || current_user @user = User.find_by(id: params[:user_id]) || User.find_by(id: params[:id]) || current_user
end end

53
app/helpers/duplicate_logs_helper.rb

@ -8,10 +8,13 @@ module DuplicateLogsHelper
action: "delete_duplicates", action: "delete_duplicates",
"#{duplicate_log.class.name.underscore}_id": duplicate_log.id, "#{duplicate_log.class.name.underscore}_id": duplicate_log.id,
original_log_id: original_log.id, original_log_id: original_log.id,
referrer: params[:referrer],
organisation_id: params[:organisation_id],
) )
end end
if params[:referrer] == "duplicate_logs_banner"
if !original_log.deleted? current_user.support? ? govuk_button_link_to("Review other duplicates", organisation_duplicates_path(organisation_id: params[:organisation_id], referrer: params[:referrer])) : govuk_button_link_to("Review other duplicates", duplicate_logs_path(referrer: params[:referrer]))
elsif !original_log.deleted?
govuk_button_link_to "Back to Log #{original_log.id}", send("#{original_log.class.name.underscore}_path", original_log) govuk_button_link_to "Back to Log #{original_log.id}", send("#{original_log.class.name.underscore}_path", original_log)
else else
type = duplicate_log.lettings? ? "lettings" : "sales" type = duplicate_log.lettings? ? "lettings" : "sales"
@ -25,7 +28,7 @@ module DuplicateLogsHelper
def change_duplicate_logs_action_href(log, page_id, all_duplicates, original_log_id) def change_duplicate_logs_action_href(log, page_id, all_duplicates, original_log_id)
first_remaining_duplicate_id = all_duplicates.map(&:id).reject { |id| id == log.id }.first first_remaining_duplicate_id = all_duplicates.map(&:id).reject { |id| id == log.id }.first
send("#{log.model_name.param_key}_#{page_id}_path", log, referrer: "duplicate_logs", first_remaining_duplicate_id:, original_log_id:) send("#{log.model_name.param_key}_#{page_id}_path", log, referrer: params[:referrer] == "duplicate_logs_banner" ? "duplicate_logs_banner" : "duplicate_logs", first_remaining_duplicate_id:, original_log_id:, organisation_id: params[:organisation_id])
end end
def duplicates_for_user(user) def duplicates_for_user(user)
@ -50,4 +53,48 @@ module DuplicateLogsHelper
def duplicate_list_header(duplicate_sets_count) def duplicate_list_header(duplicate_sets_count)
duplicate_sets_count > 1 ? "Review these #{duplicate_sets_count} sets of logs" : "Review this #{duplicate_sets_count} set of logs" duplicate_sets_count > 1 ? "Review these #{duplicate_sets_count} sets of logs" : "Review this #{duplicate_sets_count} set of logs"
end end
def duplicate_log_question_label(question)
if question.id == "uprn"
"Postcode (from UPRN)"
else
get_question_label(question)
end
end
def duplicate_log_answer_label(question, log)
if question.id == "uprn"
postcode_question = log.form.get_question("postcode_full", log)
get_answer_label(postcode_question, log)
else
get_answer_label(question, log)
end
end
def duplicate_log_extra_value(question, log)
if question.id == "uprn"
postcode_question = log.form.get_question("postcode_full", log)
postcode_question.get_extra_check_answer_value(log)
else
question.get_extra_check_answer_value(log)
end
end
def duplicate_log_answer_label_present(question, log, current_user)
if question.id == "uprn"
postcode_question = log.form.get_question("postcode_full", log)
postcode_question.answer_label(log, current_user).present?
else
question.answer_label(log, current_user).present?
end
end
def duplicate_log_inferred_answers(question, log)
if question.id == "uprn"
postcode_question = log.form.get_question("postcode_full", log)
postcode_question.get_inferred_answers(log)
else
question.get_inferred_answers(log)
end
end
end end

35
app/helpers/filters_helper.rb

@ -26,6 +26,7 @@ module FiltersHelper
filters["organisation"].present? || filters["organisation"].present? ||
filters["managing_organisation"].present? || filters["managing_organisation"].present? ||
filters["status"]&.compact_blank&.any? || filters["status"]&.compact_blank&.any? ||
filters["needstypes"]&.compact_blank&.any? ||
filters["years"]&.compact_blank&.any? || filters["years"]&.compact_blank&.any? ||
filters["bulk_upload_id"].present? filters["bulk_upload_id"].present?
end end
@ -56,6 +57,13 @@ module FiltersHelper
}.freeze }.freeze
end end
def needstype_filters
{
"1" => "General needs",
"2" => "Supported housing",
}.freeze
end
def location_status_filters def location_status_filters
{ {
"incomplete" => "Incomplete", "incomplete" => "Incomplete",
@ -108,12 +116,35 @@ module FiltersHelper
user.support? || org.stock_owners.count > 1 || (org.holds_own_stock? && org.stock_owners.count.positive?) user.support? || org.stock_owners.count > 1 || (org.holds_own_stock? && org.stock_owners.count.positive?)
end end
private def logs_for_both_needstypes_present?(organisation)
return true if current_user.support? && organisation.blank?
return [1, 2].all? { |needstype| organisation.lettings_logs.visible.where(needstype:).count.positive? } if current_user.support?
[1, 2].all? { |needstype| current_user.lettings_logs.visible.where(needstype:).count.positive? }
end
def non_support_with_multiple_owning_orgs?
current_user.organisation.stock_owners.count > 1 && user_lettings_path?
end
def non_support_with_multiple_managing_orgs?
current_user.organisation.managing_agents.count > 1 && user_lettings_path?
end
def user_lettings_path?
request.path == "/lettings-logs"
end
def user_or_org_lettings_path?
request.path.include?("/lettings-logs")
end
def applied_filters_count(filter_type) def applied_filters_count(filter_type)
filters_count(applied_filters(filter_type)) filters_count(applied_filters(filter_type))
end end
private
def applied_filters(filter_type) def applied_filters(filter_type)
return {} unless session[session_name_for(filter_type)] return {} unless session[session_name_for(filter_type)]
@ -126,7 +157,7 @@ private
def filters_count(filters) def filters_count(filters)
filters.each.sum do |category, category_filters| filters.each.sum do |category, category_filters|
if %w[status years bulk_upload_id].include?(category) if %w[years status needstypes bulk_upload_id].include?(category)
category_filters.count(&:present?) category_filters.count(&:present?)
elsif %w[user owning_organisation managing_organisation].include?(category) elsif %w[user owning_organisation managing_organisation].include?(category)
1 1

8
app/helpers/form_page_helper.rb

@ -2,4 +2,12 @@ module FormPageHelper
def action_href(log, page_id, referrer = "check_answers") def action_href(log, page_id, referrer = "check_answers")
send("#{log.model_name.param_key}_#{page_id}_path", log, referrer:) send("#{log.model_name.param_key}_#{page_id}_path", log, referrer:)
end end
def returning_to_question_page?(page, referrer)
page.interruption_screen? || referrer == "check_answers"
end
def accessed_from_duplicate_logs?(referrer)
%w[duplicate_logs duplicate_logs_banner].include?(referrer)
end
end end

13
app/models/derived_variables/lettings_log_variables.rb

@ -45,6 +45,8 @@ module DerivedVariables::LettingsLogVariables
end end
self.renttype = RENT_TYPE_MAPPING[rent_type] self.renttype = RENT_TYPE_MAPPING[rent_type]
self.lettype = get_lettype self.lettype = get_lettype
self.lar = get_lar
self.irproduct = get_irproduct
self.totchild = get_totchild self.totchild = get_totchild
self.totelder = get_totelder self.totelder = get_totelder
self.totadult = get_totadult self.totadult = get_totadult
@ -307,4 +309,15 @@ private
self.town_or_city = nil self.town_or_city = nil
self.county = nil self.county = nil
end end
def get_lar
return 1 if rent_type == 2
return 2 if rent_type == 1
end
def get_irproduct
return 1 if rent_type == 3
return 2 if rent_type == 4
return 3 if rent_type == 5
end
end end

3
app/models/form/lettings/pages/address.rb

@ -16,9 +16,8 @@ class Form::Lettings::Pages::Address < ::Form::Page
end end
def routed_to?(log, _current_user = nil) def routed_to?(log, _current_user = nil)
return false if log.uprn_known.nil?
return false if log.is_supported_housing? return false if log.is_supported_housing?
log.uprn_confirmed != 1 || log.uprn_known.zero? log.uprn_known.nil? || log.uprn_known.zero? || log.uprn_confirmed&.zero?
end end
end end

1
app/models/form/lettings/pages/location.rb

@ -7,6 +7,7 @@ class Form::Lettings::Pages::Location < ::Form::Page
"scheme_has_multiple_locations?" => true, "scheme_has_multiple_locations?" => true,
}, },
] ]
@header = "Location"
@next_unresolved_page_id = :check_answers @next_unresolved_page_id = :check_answers
end end

2
app/models/form/lettings/pages/max_rent_value_check.rb

@ -19,6 +19,6 @@ class Form::Lettings::Pages::MaxRentValueCheck < ::Form::Page
end end
def interruption_screen_question_ids def interruption_screen_question_ids
%w[brent startdate uprn postcode_full la beds rent_type needstype] %w[brent period startdate uprn postcode_full la beds rent_type needstype]
end end
end end

2
app/models/form/lettings/pages/min_rent_value_check.rb

@ -19,6 +19,6 @@ class Form::Lettings::Pages::MinRentValueCheck < ::Form::Page
end end
def interruption_screen_question_ids def interruption_screen_question_ids
%w[brent startdate uprn postcode_full la beds rent_type needstype] %w[brent period startdate uprn postcode_full la beds rent_type needstype]
end end
end end

1
app/models/form/lettings/questions/location_id.rb

@ -13,6 +13,7 @@ class Form::Lettings::Questions::LocationId < ::Form::Question
} }
@question_number = 10 @question_number = 10
@disable_clearing_if_not_routed_or_dynamic_answer_options = true @disable_clearing_if_not_routed_or_dynamic_answer_options = true
@top_guidance_partial = "finding_location"
end end
def answer_options def answer_options

4
app/models/form/lettings/questions/managing_organisation.rb

@ -23,8 +23,8 @@ class Form::Lettings::Questions::ManagingOrganisation < ::Form::Question
if log.owning_organisation.holds_own_stock? if log.owning_organisation.holds_own_stock?
opts[log.owning_organisation.id] = "#{log.owning_organisation.name} (Owning organisation)" opts[log.owning_organisation.id] = "#{log.owning_organisation.name} (Owning organisation)"
end end
elsif user.organisation.absorbed_organisations.exists? elsif user.organisation.absorbed_organisations.exists? && user.organisation.available_from.present?
opts[user.organisation.id] = "#{user.organisation.name} (Your organisation, active as of #{user.organisation.created_at.to_fs(:govuk_date)})" opts[user.organisation.id] = "#{user.organisation.name} (Your organisation, active as of #{user.organisation.available_from.to_fs(:govuk_date)})"
else else
opts[user.organisation.id] = "#{user.organisation.name} (Your organisation)" opts[user.organisation.id] = "#{user.organisation.name} (Your organisation)"
end end

1
app/models/form/lettings/questions/referral.rb

@ -14,6 +14,7 @@ class Form::Lettings::Questions::Referral < ::Form::Question
ANSWER_OPTIONS = { ANSWER_OPTIONS = {
"1" => { "1" => {
"value" => "Internal transfer", "value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
}, },
"2" => { "2" => {
"value" => "Tenant applied directly (no referral or nomination)", "value" => "Tenant applied directly (no referral or nomination)",

1
app/models/form/lettings/questions/referral_prp.rb

@ -14,6 +14,7 @@ class Form::Lettings::Questions::ReferralPrp < ::Form::Question
ANSWER_OPTIONS = { ANSWER_OPTIONS = {
"1" => { "1" => {
"value" => "Internal transfer", "value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
}, },
"2" => { "2" => {
"value" => "Tenant applied directly (no nomination)", "value" => "Tenant applied directly (no nomination)",

1
app/models/form/lettings/questions/referral_supported_housing.rb

@ -14,6 +14,7 @@ class Form::Lettings::Questions::ReferralSupportedHousing < ::Form::Question
ANSWER_OPTIONS = { ANSWER_OPTIONS = {
"1" => { "1" => {
"value" => "Internal transfer", "value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
}, },
"2" => { "2" => {
"value" => "Tenant applied directly (no referral)", "value" => "Tenant applied directly (no referral)",

2
app/models/form/lettings/questions/referral_supported_housing_prp.rb

@ -12,7 +12,7 @@ class Form::Lettings::Questions::ReferralSupportedHousingPrp < ::Form::Question
end end
ANSWER_OPTIONS = { ANSWER_OPTIONS = {
"1" => { "value" => "Internal transfer" }, "1" => { "value" => "Internal transfer", "hint" => "Where the tenant has moved to another social property owned by the same landlord." },
"2" => { "value" => "Tenant applied directly (no referral or nomination)" }, "2" => { "value" => "Tenant applied directly (no referral or nomination)" },
"3" => { "value" => "Nominated by a local housing authority" }, "3" => { "value" => "Nominated by a local housing authority" },
"4" => { "value" => "Referred by local authority housing department" }, "4" => { "value" => "Referred by local authority housing department" },

38
app/models/form/lettings/questions/stock_owner.rb

@ -16,20 +16,38 @@ class Form::Lettings::Questions::StockOwner < ::Form::Question
return answer_opts unless log return answer_opts unless log
if log.owning_organisation_id.present? if log.owning_organisation_id.present?
answer_opts = answer_opts.merge({ log.owning_organisation.id => log.owning_organisation.name }) answer_opts[log.owning_organisation.id] = log.owning_organisation.name
end end
recently_absorbed_organisations = user.organisation.absorbed_organisations.merged_during_open_collection_period
if !user.support? && user.organisation.holds_own_stock? if !user.support? && user.organisation.holds_own_stock?
answer_opts[user.organisation.id] = "#{user.organisation.name} (Your organisation)" answer_opts[user.organisation.id] = if recently_absorbed_organisations.exists? && user.organisation.available_from.present?
"#{user.organisation.name} (Your organisation, active as of #{user.organisation.available_from.to_fs(:govuk_date)})"
else
"#{user.organisation.name} (Your organisation)"
end
end end
user_answer_options = if user.support? if user.support?
Organisation.where(holds_own_stock: true) Organisation.where(holds_own_stock: true).find_each do |org|
else if org.merge_date.present?
user.organisation.stock_owners + user.organisation.absorbed_organisations.where(holds_own_stock: true) answer_opts[org.id] = "#{org.name} (inactive as of #{org.merge_date.to_fs(:govuk_date)})" if org.merge_date >= FormHandler.instance.start_date_of_earliest_open_for_editing_collection_period
end.pluck(:id, :name).to_h elsif org.absorbed_organisations.merged_during_open_collection_period.exists?
answer_opts[org.id] = "#{org.name} (active as of #{org.created_at.to_fs(:govuk_date)})"
else
answer_opts[org.id] = org.name
end
end
else
user.organisation.stock_owners.each do |stock_owner|
answer_opts[stock_owner.id] = stock_owner.name
end
recently_absorbed_organisations.each do |absorbed_org|
answer_opts[absorbed_org.id] = merged_organisation_label(absorbed_org.name, absorbed_org.merge_date) if absorbed_org.holds_own_stock?
end
end
answer_opts.merge(user_answer_options) answer_opts
end end
def displayed_answer_options(log, user = nil) def displayed_answer_options(log, user = nil)
@ -71,4 +89,8 @@ private
def selected_answer_option_is_derived?(_log) def selected_answer_option_is_derived?(_log)
true true
end end
def merged_organisation_label(name, merge_date)
"#{name} (inactive as of #{merge_date.to_fs(:govuk_date)})"
end
end end

4
app/models/form/sales/pages/address.rb

@ -16,8 +16,6 @@ class Form::Sales::Pages::Address < ::Form::Page
end end
def routed_to?(log, _current_user = nil) def routed_to?(log, _current_user = nil)
return false if log.uprn_known.nil? log.uprn_known.nil? || log.uprn_known.zero? || log.uprn_confirmed&.zero?
log.uprn_known.zero? || log.uprn_confirmed&.zero?
end end
end end

4
app/models/form/sales/pages/uprn.rb

@ -11,6 +11,10 @@ class Form::Sales::Pages::Uprn < ::Form::Page
] ]
end end
def routed_to?(_log, _current_user)
true
end
def skip_text def skip_text
"Enter address instead" "Enter address instead"
end end

4
app/models/form/sales/questions/owning_organisation_id.rb

@ -22,8 +22,8 @@ class Form::Sales::Questions::OwningOrganisationId < ::Form::Question
recently_absorbed_organisations = user.organisation.absorbed_organisations.merged_during_open_collection_period recently_absorbed_organisations = user.organisation.absorbed_organisations.merged_during_open_collection_period
if !user.support? && user.organisation.holds_own_stock? if !user.support? && user.organisation.holds_own_stock?
answer_opts[user.organisation.id] = if recently_absorbed_organisations.exists? answer_opts[user.organisation.id] = if recently_absorbed_organisations.exists? && user.organisation.available_from.present?
"#{user.organisation.name} (Your organisation, active as of #{user.organisation.created_at.to_fs(:govuk_date)})" "#{user.organisation.name} (Your organisation, active as of #{user.organisation.available_from.to_fs(:govuk_date)})"
else else
"#{user.organisation.name} (Your organisation)" "#{user.organisation.name} (Your organisation)"
end end

23
app/models/lettings_log.rb

@ -43,6 +43,13 @@ class LettingsLog < Log
scope :filter_by_tenant_code, ->(tenant_code) { where("tenancycode ILIKE ?", "%#{tenant_code}%") } scope :filter_by_tenant_code, ->(tenant_code) { where("tenancycode ILIKE ?", "%#{tenant_code}%") }
scope :filter_by_propcode, ->(propcode) { where("propcode ILIKE ?", "%#{propcode}%") } scope :filter_by_propcode, ->(propcode) { where("propcode ILIKE ?", "%#{propcode}%") }
scope :filter_by_location_postcode, ->(postcode_full) { left_joins(:location).where("REPLACE(locations.postcode, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") } scope :filter_by_location_postcode, ->(postcode_full) { left_joins(:location).where("REPLACE(locations.postcode, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") }
scope :filter_by_needstype, ->(needstype) { where(needstype:) }
scope :filter_by_needstypes, lambda { |needstypes, _user = nil|
first_needstype = needstypes.shift
query = filter_by_needstype(first_needstype)
needstypes.each { |needstype| query = query.or(filter_by_needstype(needstype)) }
query.all
}
scope :search_by, lambda { |param| scope :search_by, lambda { |param|
filter_by_location_postcode(param) filter_by_location_postcode(param)
.or(filter_by_tenant_code(param)) .or(filter_by_tenant_code(param))
@ -594,6 +601,22 @@ class LettingsLog < Log
public_send("details_known_#{person_index}") == 1 public_send("details_known_#{person_index}") == 1
end end
def duplicate_check_question_ids
["owning_organisation_id",
"startdate",
"tenancycode",
form.start_date.year < 2023 || uprn.blank? ? "postcode_full" : nil,
form.start_date.year >= 2023 && uprn.present? ? "uprn" : nil,
"scheme_id",
"location_id",
"age1",
"sex1",
"ecstat1",
household_charge == 1 ? "household_charge" : nil,
"tcharge",
is_carehome? ? "chcharge" : nil].compact
end
private private
def reset_invalid_unresolved_log_fields! def reset_invalid_unresolved_log_fields!

10
app/models/location.rb

@ -16,7 +16,7 @@ class Location < ApplicationRecord
before_validation :lookup_postcode!, if: :postcode_changed? before_validation :lookup_postcode!, if: :postcode_changed?
auto_strip_attributes :name auto_strip_attributes :name, squish: true
scope :search_by_postcode, ->(postcode) { where("REPLACE(postcode, ' ', '') ILIKE ?", "%#{postcode.delete(' ')}%") } scope :search_by_postcode, ->(postcode) { where("REPLACE(postcode, ' ', '') ILIKE ?", "%#{postcode.delete(' ')}%") }
scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") } scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
@ -110,7 +110,7 @@ class Location < ApplicationRecord
def self.find_by_id_on_multiple_fields(id) def self.find_by_id_on_multiple_fields(id)
return if id.nil? return if id.nil?
where(id:).or(where(old_visible_id: id)).first where(id:).or(where("ltrim(old_visible_id, '0') = ?", id.to_i.to_s)).first
end end
def postcode=(postcode) def postcode=(postcode)
@ -131,8 +131,8 @@ class Location < ApplicationRecord
location_deactivation_periods.deactivations_without_reactivation.first location_deactivation_periods.deactivations_without_reactivation.first
end end
def recent_deactivation def last_deactivation_before(date)
location_deactivation_periods.order("created_at").last location_deactivation_periods.where("deactivation_date <= ?", date).order("created_at").last
end end
def status def status
@ -143,7 +143,7 @@ class Location < ApplicationRecord
return :incomplete unless confirmed return :incomplete unless confirmed
return :deactivated if open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date return :deactivated if 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 :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date
return :reactivating_soon if recent_deactivation&.reactivation_date.present? && date < recent_deactivation.reactivation_date return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date
return :activating_soon if startdate.present? && date < startdate return :activating_soon if startdate.present? && date < startdate
:active :active

7
app/models/log.rb

@ -193,6 +193,13 @@ class Log < ApplicationRecord
form.edit_end_date < Time.zone.now || older_than_previous_collection_year? form.edit_end_date < Time.zone.now || older_than_previous_collection_year?
end end
def duplicate_check_questions(current_user)
duplicate_check_question_ids.map { |question_id|
question = form.get_question(question_id, self)
question if question.page.routed_to?(self, current_user)
}.compact
end
private private
# Handle logs that are older than previous collection start date # Handle logs that are older than previous collection start date

8
app/models/organisation.rb

@ -36,7 +36,7 @@ class Organisation < ApplicationRecord
has_paper_trail has_paper_trail
auto_strip_attributes :name auto_strip_attributes :name, squish: true
PROVIDER_TYPE = { PROVIDER_TYPE = {
LA: 1, LA: 1,
@ -140,4 +140,10 @@ class Organisation < ApplicationRecord
def duplicate_sales_logs_sets def duplicate_sales_logs_sets
sales_logs.duplicate_sets.map { |array_str| array_str ? array_str.map(&:to_i) : [] } sales_logs.duplicate_sets.map { |array_str| array_str ? array_str.map(&:to_i) : [] }
end end
def recently_absorbed_organisations_grouped_by_merge_date
return unless absorbed_organisations.present? && absorbed_organisations.merged_during_open_collection_period.present?
absorbed_organisations.merged_during_open_collection_period.group_by(&:merge_date)
end
end end

11
app/models/sales_log.rb

@ -436,4 +436,15 @@ class SalesLog < Log
self.pcodenk = nil if errors.attribute_names.include? :postcode_full self.pcodenk = nil if errors.attribute_names.include? :postcode_full
end end
def duplicate_check_question_ids
["owning_organisation_id",
"saledate",
"purchid",
"age1",
"sex1",
"ecstat1",
form.start_date.year < 2023 || uprn.blank? ? "postcode_full" : nil,
form.start_date.year >= 2023 && uprn.present? ? "uprn" : nil].compact
end
end end

23
app/models/scheme.rb

@ -75,7 +75,7 @@ class Scheme < ApplicationRecord
validate :validate_confirmed validate :validate_confirmed
validate :validate_owning_organisation validate :validate_owning_organisation
auto_strip_attributes :service_name auto_strip_attributes :service_name, squish: true
SENSITIVE = { SENSITIVE = {
No: 0, No: 0,
@ -109,6 +109,7 @@ class Scheme < ApplicationRecord
"Medium level": 3, "Medium level": 3,
"High level": 4, "High level": 4,
"Nursing care in a care home": 5, "Nursing care in a care home": 5,
"Floating support": 6,
}.freeze }.freeze
enum support_type: SUPPORT_TYPE, _suffix: true enum support_type: SUPPORT_TYPE, _suffix: true
@ -162,13 +163,15 @@ class Scheme < ApplicationRecord
enum arrangement_type: ARRANGEMENT_TYPE, _suffix: true enum arrangement_type: ARRANGEMENT_TYPE, _suffix: true
def self.find_by_id_on_multiple_fields(id) def self.find_by_id_on_multiple_fields(scheme_id, location_id)
return if id.nil? return if scheme_id.nil?
if id.start_with?("S") if scheme_id.start_with?("S")
where(id: id[1..]).first where(id: scheme_id[1..]).first
elsif location_id.present?
joins(:locations).where("ltrim(schemes.old_visible_id, '0') = ? AND ltrim(locations.old_visible_id, '0') = ?", scheme_id.to_i.to_s, location_id.to_i.to_s).first || where("ltrim(schemes.old_visible_id, '0') = ?", scheme_id.to_i.to_s).first
else else
where(old_visible_id: id).first where("ltrim(old_visible_id, '0') = ?", scheme_id.to_i.to_s).first
end end
end end
@ -225,7 +228,7 @@ class Scheme < ApplicationRecord
"Medium level": "Staff on site daily or making frequent visits with some out-of-hours cover.", "Medium level": "Staff on site daily or making frequent visits with some out-of-hours cover.",
"High level": "Intensive level of staffing provided on a 24-hour basis.", "High level": "Intensive level of staffing provided on a 24-hour basis.",
} }
Scheme.support_types.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize, description: hints[key.to_sym]) } Scheme.support_types.keys.excluding("Missing").excluding("Floating support").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize, description: hints[key.to_sym]) }
end end
def intended_length_of_stay_options_with_hints def intended_length_of_stay_options_with_hints
@ -266,8 +269,8 @@ class Scheme < ApplicationRecord
scheme_deactivation_periods.deactivations_without_reactivation.first scheme_deactivation_periods.deactivations_without_reactivation.first
end end
def recent_deactivation def last_deactivation_before(date)
scheme_deactivation_periods.order("created_at").last scheme_deactivation_periods.where("deactivation_date <= ?", date).order("created_at").last
end end
def status def status
@ -278,7 +281,7 @@ class Scheme < ApplicationRecord
return :incomplete unless confirmed && locations.confirmed.any? return :incomplete unless confirmed && locations.confirmed.any?
return :deactivated if open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date return :deactivated if 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 :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date
return :reactivating_soon if recent_deactivation&.reactivation_date.present? && date < recent_deactivation.reactivation_date return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date
:active :active
end end

6
app/models/user.rb

@ -38,7 +38,7 @@ class User < ApplicationRecord
has_one_time_password(encrypted: true) has_one_time_password(encrypted: true)
auto_strip_attributes :name auto_strip_attributes :name, squish: true
ROLES = { ROLES = {
data_provider: 1, data_provider: 1,
@ -173,9 +173,9 @@ class User < ApplicationRecord
def logs_filters(specific_org: false) def logs_filters(specific_org: false)
if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners? if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners?
%w[status years assigned_to user managing_organisation owning_organisation bulk_upload_id] %w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id]
else else
%w[status years assigned_to user bulk_upload_id] %w[years status needstypes assigned_to user bulk_upload_id]
end end
end end

20
app/models/validations/financial_validations.rb

@ -146,7 +146,7 @@ module Validations::FinancialValidations
private private
CHARGE_MAXIMUMS = { CHARGE_MAXIMA_PER_WEEK = {
scharge: { scharge: {
private_registered_provider: { private_registered_provider: {
general_needs: 800, general_needs: 800,
@ -181,22 +181,30 @@ private
PROVIDER_TYPE = { 1 => :local_authority, 2 => :private_registered_provider }.freeze PROVIDER_TYPE = { 1 => :local_authority, 2 => :private_registered_provider }.freeze
NEEDSTYPE_VALUES = { 2 => :supported_housing, 1 => :general_needs }.freeze NEEDSTYPE_VALUES = { 2 => :supported_housing, 1 => :general_needs }.freeze
CHARGE_NAMES = { scharge: "service charge", pscharge: "personal service charge", supcharg: "support charge" }.freeze
def validate_charges(record) def validate_charges(record)
return unless record.owning_organisation return unless record.owning_organisation
provider_type = record.owning_organisation.provider_type_before_type_cast provider_type = record.owning_organisation.provider_type_before_type_cast
%i[scharge pscharge supcharg].each do |charge| %i[scharge pscharge supcharg].each do |charge|
maximum = CHARGE_MAXIMUMS.dig(charge, PROVIDER_TYPE[provider_type], NEEDSTYPE_VALUES[record.needstype]) maximum_per_week = CHARGE_MAXIMA_PER_WEEK.dig(charge, PROVIDER_TYPE[provider_type], NEEDSTYPE_VALUES[record.needstype])
if maximum.present? && record[:period].present? && record[charge].present? && !weekly_value_in_range(record, charge, 0.0, maximum) next unless maximum_per_week.present? && record[:period].present? && record[charge].present? && !weekly_value_in_range(record, charge, 0.0, maximum_per_week)
record.errors.add charge, :outside_the_range, message: I18n.t("validations.financial.rent.#{charge}.#{PROVIDER_TYPE[provider_type]}.#{NEEDSTYPE_VALUES[record.needstype]}")
end charge_name = CHARGE_NAMES[charge]
frequency = record.form.get_question("period", record).label_from_value(record.period).downcase
letting_type = NEEDSTYPE_VALUES[record.needstype].to_s.humanize(capitalize: false)
provider_type_label = PROVIDER_TYPE[provider_type].to_s.humanize(capitalize: false)
maximum_per_period = record.weekly_to_value_per_period(maximum_per_week)
record.errors.add charge, :outside_the_range, message: I18n.t("validations.financial.rent.out_of_range", charge_name:, maximum_per_period:, frequency:, letting_type:, provider_type: provider_type_label)
record.errors.add :period, :outside_the_range, message: I18n.t("validations.financial.rent.out_of_range", charge_name:, maximum_per_period:, frequency:, letting_type:, provider_type: provider_type_label)
end end
end end
def weekly_value_in_range(record, field, min, max) def weekly_value_in_range(record, field, min, max)
record[field].present? && record.weekly_value(record[field]).present? && record.weekly_value(record[field]).between?(min, max) record.weekly_value(record[field])&.between?(min, max)
end end
def validate_rent_range(record) def validate_rent_range(record)

3
app/models/validations/household_validations.rb

@ -73,7 +73,6 @@ module Validations::HouseholdValidations
# 13 Children's home / Foster Care # 13 Children's home / Foster Care
# 14 Bed and breakfast # 14 Bed and breakfast
# 19 Rough Sleeping # 19 Rough Sleeping
# 21 Refuge
# 23 Mobile home / Caravan # 23 Mobile home / Caravan
# 24 Home Office Asylum Support # 24 Home Office Asylum Support
# 25 Other # 25 Other
@ -81,7 +80,7 @@ module Validations::HouseholdValidations
# 27 Owner occupation (low-cost home ownership) # 27 Owner occupation (low-cost home ownership)
# 28 Living with Friends or Family # 28 Living with Friends or Family
# 29 Prison / Approved Probation Hostel # 29 Prison / Approved Probation Hostel
if record.is_internal_transfer? && [3, 4, 7, 10, 13, 14, 19, 21, 23, 24, 25, 26, 27, 28, 29].include?(record.prevten) if record.is_internal_transfer? && [3, 4, 7, 10, 13, 14, 19, 23, 24, 25, 26, 27, 28, 29].include?(record.prevten)
label = record.form.get_question("prevten", record).present? ? record.form.get_question("prevten", record).label_from_value(record.prevten) : "" label = record.form.get_question("prevten", record).present? ? record.form.get_question("prevten", record).label_from_value(record.prevten) : ""
record.errors.add :prevten, :internal_transfer_non_social_housing, message: I18n.t("validations.household.prevten.internal_transfer", prevten: label) record.errors.add :prevten, :internal_transfer_non_social_housing, message: I18n.t("validations.household.prevten.internal_transfer", prevten: label)
record.errors.add :referral, :internal_transfer_non_social_housing, message: I18n.t("validations.household.referral.prevten_invalid", prevten: label) record.errors.add :referral, :internal_transfer_non_social_housing, message: I18n.t("validations.household.referral.prevten_invalid", prevten: label)

8
app/models/validations/sales/setup_validations.rb

@ -37,7 +37,7 @@ module Validations::Sales::SetupValidations
if absorbing_owning_organisation_inactive?(record) if absorbing_owning_organisation_inactive?(record)
record.errors.add :saledate, I18n.t("validations.setup.saledate.invalid_absorbing_organisations_saledate", record.errors.add :saledate, I18n.t("validations.setup.saledate.invalid_absorbing_organisations_saledate",
owning_organisation: record.owning_organisation.name, owning_organisation: record.owning_organisation.name,
owning_organisation_available_from: record.owning_organisation.created_at.to_formatted_s(:govuk_date)) owning_organisation_available_from: record.owning_organisation.available_from.to_formatted_s(:govuk_date))
end end
end end
@ -50,10 +50,10 @@ module Validations::Sales::SetupValidations
owning_organisation: record.owning_organisation.name, owning_organisation: record.owning_organisation.name,
owning_organisation_merge_date: record.owning_organisation.merge_date.to_formatted_s(:govuk_date), owning_organisation_merge_date: record.owning_organisation.merge_date.to_formatted_s(:govuk_date),
owning_absorbing_organisation: record.owning_organisation.absorbing_organisation.name) owning_absorbing_organisation: record.owning_organisation.absorbing_organisation.name)
elsif record.owning_organisation&.absorbed_organisations.present? && record.owning_organisation.created_at.to_date > record.saledate.to_date elsif record.owning_organisation&.absorbed_organisations.present? && record.owning_organisation.available_from.present? && record.owning_organisation.available_from.to_date > record.saledate.to_date
record.errors.add :owning_organisation_id, I18n.t("validations.setup.owning_organisation.inactive_absorbing_organisation_sales", record.errors.add :owning_organisation_id, I18n.t("validations.setup.owning_organisation.inactive_absorbing_organisation_sales",
owning_organisation: record.owning_organisation.name, owning_organisation: record.owning_organisation.name,
owning_organisation_available_from: record.owning_organisation.created_at.to_formatted_s(:govuk_date)) owning_organisation_available_from: record.owning_organisation.available_from.to_formatted_s(:govuk_date))
end end
end end
end end
@ -104,6 +104,6 @@ private
end end
def absorbing_owning_organisation_inactive?(record) def absorbing_owning_organisation_inactive?(record)
record.owning_organisation&.absorbed_organisations.present? && record.owning_organisation.created_at.to_date > record.saledate.to_date record.owning_organisation&.absorbed_organisations.present? && record.owning_organisation.available_from.present? && record.owning_organisation.available_from.to_date > record.saledate.to_date
end end
end end

20
app/models/validations/setup_validations.rb

@ -61,10 +61,10 @@ module Validations::SetupValidations
owning_organisation: record.owning_organisation.name, owning_organisation: record.owning_organisation.name,
owning_organisation_merge_date: record.owning_organisation.merge_date.to_formatted_s(:govuk_date), owning_organisation_merge_date: record.owning_organisation.merge_date.to_formatted_s(:govuk_date),
owning_absorbing_organisation: record.owning_organisation.absorbing_organisation.name) owning_absorbing_organisation: record.owning_organisation.absorbing_organisation.name)
elsif owning_organisation&.absorbed_organisations.present? && owning_organisation.created_at.to_date > record.startdate.to_date elsif owning_organisation&.absorbed_organisations.present? && owning_organisation.available_from.present? && owning_organisation.available_from.to_date > record.startdate.to_date
record.errors.add :owning_organisation_id, I18n.t("validations.setup.owning_organisation.inactive_absorbing_organisation", record.errors.add :owning_organisation_id, I18n.t("validations.setup.owning_organisation.inactive_absorbing_organisation",
owning_organisation: record.owning_organisation.name, owning_organisation: record.owning_organisation.name,
owning_organisation_available_from: record.owning_organisation.created_at.to_formatted_s(:govuk_date)) owning_organisation_available_from: record.owning_organisation.available_from.to_formatted_s(:govuk_date))
end end
end end
@ -74,10 +74,10 @@ module Validations::SetupValidations
managing_organisation: record.managing_organisation.name, managing_organisation: record.managing_organisation.name,
managing_organisation_merge_date: record.managing_organisation.merge_date.to_formatted_s(:govuk_date), managing_organisation_merge_date: record.managing_organisation.merge_date.to_formatted_s(:govuk_date),
managing_absorbing_organisation: record.managing_organisation.absorbing_organisation.name) managing_absorbing_organisation: record.managing_organisation.absorbing_organisation.name)
elsif managing_organisation&.absorbed_organisations.present? && managing_organisation.created_at.to_date > record.startdate.to_date elsif managing_organisation&.absorbed_organisations.present? && managing_organisation.available_from.present? && managing_organisation.available_from.to_date > record.startdate.to_date
record.errors.add :managing_organisation_id, I18n.t("validations.setup.managing_organisation.inactive_absorbing_organisation", record.errors.add :managing_organisation_id, I18n.t("validations.setup.managing_organisation.inactive_absorbing_organisation",
managing_organisation: record.managing_organisation.name, managing_organisation: record.managing_organisation.name,
managing_organisation_available_from: record.managing_organisation.created_at.to_formatted_s(:govuk_date)) managing_organisation_available_from: record.managing_organisation.available_from.to_formatted_s(:govuk_date))
end end
end end
end end
@ -194,20 +194,20 @@ private
if absorbing_owning_organisation_inactive?(record) && absorbing_managing_organisation_inactive?(record) if absorbing_owning_organisation_inactive?(record) && absorbing_managing_organisation_inactive?(record)
record.errors.add :startdate, I18n.t("validations.setup.startdate.invalid_absorbing_organisations_start_date.different_organisations", record.errors.add :startdate, I18n.t("validations.setup.startdate.invalid_absorbing_organisations_start_date.different_organisations",
owning_organisation: record.owning_organisation.name, owning_organisation: record.owning_organisation.name,
owning_organisation_active_from: record.owning_organisation.created_at.to_formatted_s(:govuk_date), owning_organisation_active_from: record.owning_organisation.available_from.to_formatted_s(:govuk_date),
managing_organisation: record.managing_organisation.name, managing_organisation: record.managing_organisation.name,
managing_organisation_active_from: record.managing_organisation.created_at.to_formatted_s(:govuk_date)) managing_organisation_active_from: record.managing_organisation.available_from.to_formatted_s(:govuk_date))
else else
if absorbing_owning_organisation_inactive?(record) if absorbing_owning_organisation_inactive?(record)
record.errors.add :startdate, I18n.t("validations.setup.startdate.invalid_absorbing_organisations_start_date.owning_organisation", record.errors.add :startdate, I18n.t("validations.setup.startdate.invalid_absorbing_organisations_start_date.owning_organisation",
owning_organisation: record.owning_organisation.name, owning_organisation: record.owning_organisation.name,
owning_organisation_available_from: record.owning_organisation.created_at.to_formatted_s(:govuk_date)) owning_organisation_available_from: record.owning_organisation.available_from.to_formatted_s(:govuk_date))
end end
if absorbing_managing_organisation_inactive?(record) if absorbing_managing_organisation_inactive?(record)
record.errors.add :startdate, I18n.t("validations.setup.startdate.invalid_absorbing_organisations_start_date.managing_organisation", record.errors.add :startdate, I18n.t("validations.setup.startdate.invalid_absorbing_organisations_start_date.managing_organisation",
managing_organisation: record.managing_organisation.name, managing_organisation: record.managing_organisation.name,
managing_organisation_available_from: record.managing_organisation.created_at.to_formatted_s(:govuk_date)) managing_organisation_available_from: record.managing_organisation.available_from.to_formatted_s(:govuk_date))
end end
end end
end end
@ -221,11 +221,11 @@ private
end end
def absorbing_owning_organisation_inactive?(record) def absorbing_owning_organisation_inactive?(record)
record.owning_organisation&.absorbed_organisations.present? && record.owning_organisation.created_at.to_date > record.startdate.to_date record.owning_organisation&.absorbed_organisations.present? && record.owning_organisation.available_from.present? && record.owning_organisation.available_from.to_date > record.startdate.to_date
end end
def absorbing_managing_organisation_inactive?(record) def absorbing_managing_organisation_inactive?(record)
record.managing_organisation&.absorbed_organisations.present? && record.managing_organisation.created_at.to_date > record.startdate.to_date record.managing_organisation&.absorbed_organisations.present? && record.managing_organisation.available_from.present? && record.managing_organisation.available_from.to_date > record.startdate.to_date
end end
def organisations_belong_to_same_merge?(organisation_a, organisation_b) def organisations_belong_to_same_merge?(organisation_a, organisation_b)

2
app/models/validations/shared_validations.rb

@ -90,7 +90,7 @@ module Validations::SharedValidations
status = resource.status_at(date) status = resource.status_at(date)
return unless %i[reactivating_soon activating_soon deactivated].include?(status) return unless %i[reactivating_soon activating_soon deactivated].include?(status)
closest_reactivation = resource.recent_deactivation closest_reactivation = resource.last_deactivation_before(date)
open_deactivation = resource.open_deactivation open_deactivation = resource.open_deactivation
date = case status date = case status

45
app/services/bulk_upload/lettings/year2022/row_parser.rb

@ -354,12 +354,10 @@ class BulkUpload::Lettings::Year2022::RowParser
validate :validate_managing_org_exists, on: :after_log validate :validate_managing_org_exists, on: :after_log
validate :validate_managing_org_related, on: :after_log validate :validate_managing_org_related, on: :after_log
validate :validate_scheme_related, on: :after_log validate :validate_related_scheme_exists, on: :after_log
validate :validate_scheme_exists, on: :after_log
validate :validate_scheme_data_given, on: :after_log validate :validate_scheme_data_given, on: :after_log
validate :validate_location_related, on: :after_log validate :validate_related_location_exists, on: :after_log
validate :validate_location_exists, on: :after_log
validate :validate_location_data_given, on: :after_log validate :validate_location_data_given, on: :after_log
validate :validate_created_by_exists, on: :after_log validate :validate_created_by_exists, on: :after_log
@ -513,7 +511,7 @@ private
end end
def created_by def created_by
@created_by ||= User.find_by(email: field_112) @created_by ||= User.where("lower(email) = ?", field_112&.downcase).first
end end
def duplicate_check_fields def duplicate_check_fields
@ -530,53 +528,36 @@ private
].compact ].compact
end end
def validate_location_related
return if scheme.blank? || location.blank?
unless location.scheme == scheme
block_log_creation!
errors.add(:field_5, "Scheme code must relate to a location that is owned by owning organisation or managing organisation")
end
end
def location def location
return if scheme.nil? return if scheme.nil?
@location ||= scheme.locations.find_by_id_on_multiple_fields(field_5) @location ||= scheme.locations.find_by_id_on_multiple_fields(field_5)
end end
def validate_location_exists def validate_related_location_exists
if scheme && field_5.present? && location.nil? if scheme && field_5.present? && location.nil?
errors.add(:field_5, "Location could not be found with the provided scheme code", category: :setup) block_log_creation!
errors.add(:field_5, "Scheme code must relate to a scheme that is owned by the owning organisation or managing organisation", category: :setup)
end end
end end
def validate_location_data_given def validate_location_data_given
if bulk_upload.supported_housing? && field_5.blank? if bulk_upload.supported_housing? && field_5.blank?
block_log_creation!
errors.add(:field_5, I18n.t("validations.not_answered", question: "scheme code"), category: :setup) errors.add(:field_5, I18n.t("validations.not_answered", question: "scheme code"), category: :setup)
end end
end end
def validate_scheme_related def validate_related_scheme_exists
return unless field_4.present? && scheme.present? if field_4.present? && owning_organisation.present? && managing_organisation.present? && scheme.nil?
owned_by_owning_org = owning_organisation && scheme.owning_organisation == owning_organisation
owned_by_managing_org = managing_organisation && scheme.owning_organisation == managing_organisation
unless owned_by_owning_org || owned_by_managing_org
block_log_creation! block_log_creation!
errors.add(:field_4, "This management group code does not belong to your organisation, or any of your stock owners / managing agents", category: :setup) errors.add(:field_4, "This management group code does not belong to the owning organisation or managing organisation", category: :setup)
end
end
def validate_scheme_exists
if field_4.present? && scheme.nil?
errors.add(:field_4, "The management group code is not correct", category: :setup)
end end
end end
def validate_scheme_data_given def validate_scheme_data_given
if bulk_upload.supported_housing? && field_4.blank? if bulk_upload.supported_housing? && field_4.blank?
block_log_creation!
errors.add(:field_4, I18n.t("validations.not_answered", question: "management group code"), category: :setup) errors.add(:field_4, I18n.t("validations.not_answered", question: "management group code"), category: :setup)
end end
end end
@ -1488,6 +1469,8 @@ private
end end
def scheme def scheme
@scheme ||= Scheme.find_by_id_on_multiple_fields(field_4) return if field_4.nil? || owning_organisation.nil? || managing_organisation.nil?
@scheme ||= Scheme.where(id: (owning_organisation.owned_schemes + managing_organisation.owned_schemes).map(&:id)).find_by_id_on_multiple_fields(field_4, field_5)
end end
end end

47
app/services/bulk_upload/lettings/year2023/row_parser.rb

@ -387,12 +387,10 @@ class BulkUpload::Lettings::Year2023::RowParser
validate :validate_managing_org_exists, on: :after_log validate :validate_managing_org_exists, on: :after_log
validate :validate_managing_org_related, on: :after_log validate :validate_managing_org_related, on: :after_log
validate :validate_scheme_related, on: :after_log validate :validate_related_scheme_exists, on: :after_log
validate :validate_scheme_exists, on: :after_log
validate :validate_scheme_data_given, on: :after_log validate :validate_scheme_data_given, on: :after_log
validate :validate_location_related, on: :after_log validate :validate_related_location_exists, on: :after_log
validate :validate_location_exists, on: :after_log
validate :validate_location_data_given, on: :after_log validate :validate_location_data_given, on: :after_log
validate :validate_created_by_exists, on: :after_log validate :validate_created_by_exists, on: :after_log
@ -402,7 +400,7 @@ class BulkUpload::Lettings::Year2023::RowParser
validate :validate_nulls, on: :after_log validate :validate_nulls, on: :after_log
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: -> { supported_housing? }
validate :validate_incomplete_soft_validations, on: :after_log validate :validate_incomplete_soft_validations, on: :after_log
@ -542,7 +540,7 @@ private
end end
def created_by def created_by
@created_by ||= User.find_by(email: field_3) @created_by ||= User.where("lower(email) = ?", field_3&.downcase).first
end end
def validate_uprn_exists_if_any_key_address_fields_are_blank def validate_uprn_exists_if_any_key_address_fields_are_blank
@ -746,47 +744,30 @@ private
end end
end end
def validate_location_related def validate_related_location_exists
return if scheme.blank? || location.blank?
if location.scheme != scheme && location_field.present?
block_log_creation!
errors.add(location_field, "#{scheme_or_management_group.capitalize} code must relate to a #{location_or_scheme} that is owned by owning organisation or managing organisation", category: :setup)
end
end
def validate_location_exists
if scheme && location_id.present? && location.nil? && location_field.present? if scheme && location_id.present? && location.nil? && location_field.present?
errors.add(location_field, "#{location_or_scheme.capitalize} could not be found with the provided #{scheme_or_management_group} code", category: :setup) block_log_creation!
errors.add(location_field, "#{location_or_scheme.capitalize} code must relate to a #{location_or_scheme} that is owned by the owning organisation or managing organisation", category: :setup)
end end
end end
def validate_location_data_given def validate_location_data_given
if supported_housing? && location_id.blank? && location_field.present? if supported_housing? && location_id.blank? && location_field.present?
block_log_creation!
errors.add(location_field, I18n.t("validations.not_answered", question: "#{location_or_scheme} code"), category: "setup") errors.add(location_field, I18n.t("validations.not_answered", question: "#{location_or_scheme} code"), category: "setup")
end end
end end
def validate_scheme_related def validate_related_scheme_exists
return unless scheme_id.present? && scheme.present? if scheme_id.present? && scheme_field.present? && owning_organisation.present? && managing_organisation.present? && scheme.nil?
owned_by_owning_org = owning_organisation && scheme.owning_organisation == owning_organisation
owned_by_managing_org = managing_organisation && scheme.owning_organisation == managing_organisation
if !(owned_by_owning_org || owned_by_managing_org) && scheme_field.present?
block_log_creation! block_log_creation!
errors.add(scheme_field, "This #{scheme_or_management_group} code does not belong to your organisation, or any of your stock owners / managing agents", category: :setup) errors.add(scheme_field, "This #{scheme_or_management_group} code does not belong to the owning organisation or managing organisation", category: :setup)
end
end
def validate_scheme_exists
if scheme_id.present? && scheme_field.present? && scheme.nil?
errors.add(scheme_field, "The #{scheme_or_management_group} code is not correct", category: :setup)
end end
end end
def validate_scheme_data_given def validate_scheme_data_given
if supported_housing? && scheme_field.present? && scheme_id.blank? if supported_housing? && scheme_field.present? && scheme_id.blank?
block_log_creation!
errors.add(scheme_field, I18n.t("validations.not_answered", question: "#{scheme_or_management_group} code"), category: "setup") errors.add(scheme_field, I18n.t("validations.not_answered", question: "#{scheme_or_management_group} code"), category: "setup")
end end
end end
@ -1276,9 +1257,9 @@ private
end end
def scheme def scheme
return if field_16.nil? return if scheme_id.nil? || owning_organisation.nil? || managing_organisation.nil?
@scheme ||= Scheme.find_by_id_on_multiple_fields(scheme_id) @scheme ||= Scheme.where(id: (owning_organisation.owned_schemes + managing_organisation.owned_schemes).map(&:id)).find_by_id_on_multiple_fields(scheme_id, location_id)
end end
def location def location

2
app/services/bulk_upload/sales/year2022/row_parser.rb

@ -934,7 +934,7 @@ private
end end
def created_by def created_by
@created_by ||= User.find_by(email: field_93) @created_by ||= User.where("lower(email) = ?", field_93&.downcase).first
end end
def hhregres def hhregres

2
app/services/bulk_upload/sales/year2023/row_parser.rb

@ -1112,7 +1112,7 @@ private
end end
def created_by def created_by
@created_by ||= User.find_by(email: field_2) @created_by ||= User.where("lower(email) = ?", field_2&.downcase).first
end end
def previous_la_known def previous_la_known

6
app/services/csv/sales_log_csv_service.rb

@ -58,12 +58,6 @@ module Csv
end end
}.freeze }.freeze
AGE_KNOWN_FIELDS = {}.tap { |hash|
(1..6).each do |i|
hash["age#{i}"] = { "age_known_field" => "age#{i}_known" }
end
}.freeze
FIELDS_ALWAYS_EXPORTED_AS_CODES = %w[ FIELDS_ALWAYS_EXPORTED_AS_CODES = %w[
la la
prevloc prevloc

4
app/services/feature_toggle.rb

@ -37,4 +37,8 @@ class FeatureToggle
def self.duplicate_summary_enabled? def self.duplicate_summary_enabled?
!Rails.env.production? !Rails.env.production?
end end
def self.maintenance_mode_enabled?
false
end
end end

26
app/services/merge/merge_organisations_service.rb

@ -1,7 +1,9 @@
class Merge::MergeOrganisationsService class Merge::MergeOrganisationsService
def initialize(absorbing_organisation_id:, merging_organisation_ids:) def initialize(absorbing_organisation_id:, merging_organisation_ids:, merge_date: Time.zone.today, absorbing_organisation_active_from_merge_date: false)
@absorbing_organisation = Organisation.find(absorbing_organisation_id) @absorbing_organisation = Organisation.find(absorbing_organisation_id)
@merging_organisations = Organisation.find(merging_organisation_ids) @merging_organisations = Organisation.find(merging_organisation_ids)
@merge_date = merge_date || Time.zone.today
@absorbing_organisation_active_from_merge_date = absorbing_organisation_active_from_merge_date
end end
def call def call
@ -18,6 +20,7 @@ class Merge::MergeOrganisationsService
merge_sales_logs(merging_organisation) merge_sales_logs(merging_organisation)
mark_organisation_as_merged(merging_organisation) mark_organisation_as_merged(merging_organisation)
end end
@absorbing_organisation.available_from = @merge_date if @absorbing_organisation_active_from_merge_date
@absorbing_organisation.save! @absorbing_organisation.save!
log_success_message log_success_message
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
@ -65,17 +68,17 @@ private
merging_organisation.owned_schemes.each do |scheme| merging_organisation.owned_schemes.each do |scheme|
next if scheme.deactivated? next if scheme.deactivated?
new_scheme = Scheme.create!(scheme.attributes.except("id", "owning_organisation_id").merge(owning_organisation: @absorbing_organisation)) new_scheme = Scheme.create!(scheme.attributes.except("id", "owning_organisation_id", "old_id", "old_visible_id").merge(owning_organisation: @absorbing_organisation))
scheme.locations.each do |location| scheme.locations.each do |location|
new_scheme.locations << Location.new(location.attributes.except("id", "scheme_id")) unless location.deactivated? new_scheme.locations << Location.new(location.attributes.except("id", "scheme_id", "old_id", "old_visible_id")) unless location.deactivated?
end end
@merged_schemes[merging_organisation.name] << { name: new_scheme.service_name, code: new_scheme.id } @merged_schemes[merging_organisation.name] << { name: new_scheme.service_name, code: new_scheme.id }
SchemeDeactivationPeriod.create!(scheme:, deactivation_date: Time.zone.now) SchemeDeactivationPeriod.create!(scheme:, deactivation_date: @merge_date)
end end
end end
def merge_lettings_logs(merging_organisation) def merge_lettings_logs(merging_organisation)
merging_organisation.owned_lettings_logs.after_date(Time.zone.today).each do |lettings_log| merging_organisation.owned_lettings_logs.after_date(@merge_date.to_time).each do |lettings_log|
if lettings_log.scheme.present? if lettings_log.scheme.present?
scheme_to_set = @absorbing_organisation.owned_schemes.find_by(service_name: lettings_log.scheme.service_name) scheme_to_set = @absorbing_organisation.owned_schemes.find_by(service_name: lettings_log.scheme.service_name)
location_to_set = scheme_to_set.locations.find_by(name: lettings_log.location&.name, postcode: lettings_log.location&.postcode) location_to_set = scheme_to_set.locations.find_by(name: lettings_log.location&.name, postcode: lettings_log.location&.postcode)
@ -84,22 +87,23 @@ private
lettings_log.location = location_to_set if location_to_set.present? lettings_log.location = location_to_set if location_to_set.present?
end end
lettings_log.owning_organisation = @absorbing_organisation lettings_log.owning_organisation = @absorbing_organisation
lettings_log.save! lettings_log.save!(validate: false)
end end
merging_organisation.managed_lettings_logs.after_date(Time.zone.today).each do |lettings_log| merging_organisation.managed_lettings_logs.after_date(@merge_date.to_time).each do |lettings_log|
lettings_log.managing_organisation = @absorbing_organisation lettings_log.managing_organisation = @absorbing_organisation
lettings_log.save! lettings_log.save!(validate: false)
end end
end end
def merge_sales_logs(merging_organisation) def merge_sales_logs(merging_organisation)
merging_organisation.sales_logs.after_date(Time.zone.today).each do |sales_log| merging_organisation.sales_logs.after_date(@merge_date.to_time).each do |sales_log|
sales_log.update(owning_organisation: @absorbing_organisation) sales_log.owning_organisation = @absorbing_organisation
sales_log.save!(validate: false)
end end
end end
def mark_organisation_as_merged(merging_organisation) def mark_organisation_as_merged(merging_organisation)
merging_organisation.update(merge_date: Time.zone.today, absorbing_organisation: @absorbing_organisation) merging_organisation.update(merge_date: @merge_date, absorbing_organisation: @absorbing_organisation)
end end
def log_success_message def log_success_message

14
app/views/duplicate_logs/_duplicate_log_check_answers.erb

@ -1,28 +1,26 @@
<div class="x-govuk-summary-card govuk-!-margin-bottom-6"> <div class="x-govuk-summary-card govuk-!-margin-bottom-6">
<div class="x-govuk-summary-card__body"> <div class="x-govuk-summary-card__body">
<%= govuk_summary_list do |summary_list| %> <%= govuk_summary_list do |summary_list| %>
<% @duplicate_check_questions.each do |question| %> <% log.duplicate_check_questions(current_user).each do |question| %>
<% summary_list.row do |row| %> <% summary_list.row do |row| %>
<% row.key { get_question_label(question) } %> <% row.key { duplicate_log_question_label(question) } %>
<% row.value do %> <% row.value do %>
<%= simple_format( <%= simple_format(
get_answer_label(question, log), duplicate_log_answer_label(question, log),
wrapper_tag: "span", wrapper_tag: "span",
class: "govuk-!-margin-right-4", class: "govuk-!-margin-right-4",
) %> ) %>
<% extra_value = question.get_extra_check_answer_value(log) %> <% if duplicate_log_extra_value(question, log) && duplicate_log_answer_label_present(question, log, current_user) %>
<% if extra_value && question.answer_label(log, current_user).present? %>
<%= simple_format( <%= simple_format(
extra_value, duplicate_log_extra_value(question, log),
wrapper_tag: "span", wrapper_tag: "span",
class: "govuk-!-font-weight-regular app-!-colour-muted", class: "govuk-!-font-weight-regular app-!-colour-muted",
) %> ) %>
<% end %> <% end %>
<% question.get_inferred_answers(log).each do |inferred_answer| %> <% duplicate_log_inferred_answers(question, log).each do |inferred_answer| %>
<span class="govuk-!-font-weight-regular app-!-colour-muted"><%= inferred_answer %></span> <span class="govuk-!-font-weight-regular app-!-colour-muted"><%= inferred_answer %></span>
<% end %> <% end %>
<% end %> <% end %>

4
app/views/duplicate_logs/index.html.erb

@ -24,7 +24,7 @@
<% row.cell text: "Lettings" %> <% row.cell text: "Lettings" %>
<% row.cell text: duplicate_set.map { |id| "Log #{id}" }.join(", ") %> <% row.cell text: duplicate_set.map { |id| "Log #{id}" }.join(", ") %>
<% row.cell do %> <% row.cell do %>
<%= govuk_link_to "Review logs", lettings_log_duplicate_logs_path(duplicate_set.first, original_log_id: duplicate_set.first) %> <%= govuk_link_to "Review logs", lettings_log_duplicate_logs_path(duplicate_set.first, original_log_id: duplicate_set.first, referrer: params[:referrer], organisation_id: params[:organisation_id]) %>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
@ -33,7 +33,7 @@
<% row.cell text: "Sales" %> <% row.cell text: "Sales" %>
<% row.cell text: duplicate_set.map { |id| "Log #{id}" }.join(", ") %> <% row.cell text: duplicate_set.map { |id| "Log #{id}" }.join(", ") %>
<% row.cell do %> <% row.cell do %>
<%= govuk_link_to "Review logs", sales_log_duplicate_logs_path(duplicate_set.first, original_log_id: duplicate_set.first) %> <%= govuk_link_to "Review logs", sales_log_duplicate_logs_path(duplicate_set.first, original_log_id: duplicate_set.first, referrer: params[:referrer], organisation_id: params[:organisation_id]) %>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>

11
app/views/duplicate_logs/no_more_duplicates.html.erb

@ -0,0 +1,11 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-l">There are no more duplicate logs</h1>
</div>
</div>
<p class="govuk-body">
You have either changed or deleted all the duplicate logs.
</p>
<%= govuk_button_link_to "Back to all logs", lettings_logs_path %>

4
app/views/form/guidance/_finding_location.erb

@ -0,0 +1,4 @@
<%= govuk_details(summary_text: "What is a location?") do %>
<p class="govuk-body">A location is a postcode where supported housing is provided under a scheme. A scheme can have multiple locations, and a location can have multiple units at the same postcode.</p>
<p class="govuk-body"><%= govuk_link_to("Read more about schemes and locations", scheme_changes_path) %></p>
<% end %>

16
app/views/form/page.html.erb

@ -63,17 +63,19 @@
<%= f.hidden_field :interruption_page_referrer_type, value: @interruption_page_referrer_type %> <%= f.hidden_field :interruption_page_referrer_type, value: @interruption_page_referrer_type %>
<div class="govuk-button-group"> <div class="govuk-button-group">
<% if !@page.interruption_screen? && if request.query_parameters["referrer"] != "check_answers" %> <% if accessed_from_duplicate_logs?(request.query_parameters["referrer"]) %>
<%= f.govuk_submit "Save and continue" %> <%= f.govuk_submit "Save changes" %>
<%= govuk_link_to "Cancel", send("#{@log.class.name.underscore}_duplicate_logs_path", @log, original_log_id: request.query_parameters["original_log_id"]) %>
<% elsif returning_to_question_page?(@page, request.query_parameters["referrer"]) %>
<%= f.govuk_submit "Save changes" %>
<%= govuk_link_to "Cancel", send(@log.form.cancel_path(@page, @log), @log) %>
<% else %>
<%= f.govuk_submit "Save and continue" %>
<%= govuk_link_to( <%= govuk_link_to(
(@page.skip_text || "Skip for now"), (@page.skip_text || "Skip for now"),
(@page.skip_href(@log) || send(@log.form.next_page_redirect_path(@page, @log, current_user), @log)), (@page.skip_href(@log) || send(@log.form.next_page_redirect_path(@page, @log, current_user), @log)),
) %> ) %>
<% else %> <% end %>
<%= f.govuk_submit "Save changes" %>
<%= govuk_link_to "Cancel", send(@log.form.cancel_path(@page, @log), @log) %>
<% end %>
<% end %>
</div> </div>
</div> </div>
</div> </div>

12
app/views/layouts/application.html.erb

@ -91,11 +91,13 @@
navigation_classes: "govuk-header__navigation--end", navigation_classes: "govuk-header__navigation--end",
) do |component| ) do |component|
component.product_name(name: t("service_name")) component.product_name(name: t("service_name"))
if current_user.nil? unless FeatureToggle.maintenance_mode_enabled?
component.navigation_item(text: "Sign in", href: user_session_path) if current_user.nil?
else component.navigation_item(text: "Sign in", href: user_session_path)
component.navigation_item(text: "Your account", href: account_path) else
component.navigation_item(text: "Sign out", href: destroy_user_session_path) component.navigation_item(text: "Your account", href: account_path)
component.navigation_item(text: "Sign out", href: destroy_user_session_path)
end
end end
end %> end %>

6
app/views/locations/_location_filters.html.erb

@ -11,7 +11,7 @@
<%= filters_applied_text(@filter_type) %> <%= filters_applied_text(@filter_type) %>
</p> </p>
<p class="govuk-!-text-align-right govuk-grid-column-one-half"> <p class="govuk-!-text-align-right govuk-grid-column-one-half">
<%= reset_filters_link(@filter_type, { scheme_id: @scheme.id }) %> <%= reset_filters_link(@filter_type, { scheme_id: @scheme.id, search: request.params["search"] }.compact) %>
</p> </p>
</div> </div>
@ -23,6 +23,10 @@
category: "status", category: "status",
} %> } %>
<% if request.params["search"].present? %>
<%= f.hidden_field :search, value: request.params["search"] %>
<% end %>
<%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %>
<% end %> <% end %>
</div> </div>

2
app/views/locations/index.html.erb

@ -24,7 +24,7 @@
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<%= render(SearchResultCaptionComponent.new(searched: @searched, count: @pagy.count, item_label:, total_count: @total_count, item: "locations", path: request.path)) %> <%= render(SearchResultCaptionComponent.new(searched: @searched, count: @pagy.count, item_label:, total_count: @total_count, item: "locations", filters_count: applied_filters_count(@filter_type))) %>
<% end %> <% end %>
<%= table.head do |head| %> <%= table.head do |head| %>
<%= head.row do |row| %> <%= head.row do |row| %>

54
app/views/logs/_log_filters.html.erb

@ -12,7 +12,7 @@
<%= filters_applied_text(@filter_type) %> <%= filters_applied_text(@filter_type) %>
</p> </p>
<p class="govuk-!-text-align-right govuk-grid-column-one-half"> <p class="govuk-!-text-align-right govuk-grid-column-one-half">
<%= reset_filters_link(@filter_type) %> <%= reset_filters_link(@filter_type, { search: request.params["search"] }.compact) %>
</p> </p>
</div> </div>
<% if bulk_upload_options(@bulk_upload).present? %> <% if bulk_upload_options(@bulk_upload).present? %>
@ -41,29 +41,39 @@
label: "Status", label: "Status",
category: "status", category: "status",
} %> } %>
<% if logs_for_both_needstypes_present?(@organisation) && user_or_org_lettings_path? %>
<%= render partial: "filters/checkbox_filter",
locals: {
f:,
options: needstype_filters,
label: "Needs type",
category: "needstypes",
} %>
<% end %>
<% end %> <% end %>
<%= render partial: "filters/radio_filter", <%= render partial: "filters/radio_filter",
locals: { locals: {
f:, f:,
options: { options: {
"all": { label: "Any user" }, "all": { label: "Any user" },
"you": { label: "You" }, "you": { label: "You" },
"specific_user": { "specific_user": {
label: "Specific user", label: "Specific user",
conditional_filter: { conditional_filter: {
type: "select", type: "select",
label: "User", label: "User",
category: "user", category: "user",
options: assigned_to_filter_options(current_user), options: assigned_to_filter_options(current_user),
},
}, },
}, },
label: "Assigned to", },
category: "assigned_to", label: "Assigned to",
} %> category: "assigned_to",
} %>
<% if current_user.support? || current_user.organisation.stock_owners.count > 1 && request.path == "/lettings-logs" %> <% if current_user.support? || non_support_with_multiple_owning_orgs? %>
<%= render partial: "filters/radio_filter", locals: { <%= render partial: "filters/radio_filter", locals: {
f:, f:,
options: { options: {
@ -83,7 +93,7 @@
} %> } %>
<% end %> <% end %>
<% if (current_user.support? || current_user.organisation.managing_agents.count > 1) && request.path == "/lettings-logs" %> <% if (current_user.support? || non_support_with_multiple_managing_orgs?) && user_or_org_lettings_path? %>
<%= render partial: "filters/radio_filter", locals: { <%= render partial: "filters/radio_filter", locals: {
f:, f:,
options: { options: {
@ -103,6 +113,10 @@
} %> } %>
<% end %> <% end %>
<% if request.params["search"].present? %>
<%= f.hidden_field :search, value: request.params["search"] %>
<% end %>
<%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %>
<% end %> <% end %>
</div> </div>

29
app/views/logs/_log_list.html.erb

@ -1,19 +1,22 @@
<h2 class="govuk-body"> <h2 class="govuk-body">
<div class="govuk-grid-row"> <div class="govuk-grid-row">
<div class="govuk-grid-column-three-quarters"> <div class="govuk-grid-column-three-quarters">
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "logs", path: request.path)) %> <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "logs", filters_count: applied_filters_count(@filter_type))) %>
<% if logs&.any? %> <% if logs&.any? %>
<%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4" %> <%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4" %>
<% if @current_user.support? %> <% if @current_user.support? %>
<%= govuk_link_to "Download (CSV, codes only)", csv_codes_only_download_url, type: "text/csv", class: "govuk-!-margin-right-4" %> <%= govuk_link_to "Download (CSV, codes only)", csv_codes_only_download_url, type: "text/csv", class: "govuk-!-margin-right-4" %>
<% end %>
<% end %> <% end %>
<% end %> </div>
</div> <div class="govuk-grid-column-one-quarter govuk-!-text-align-right">
<div class="govuk-grid-column-one-quarter govuk-!-text-align-right"> <% if searched || applied_filters_count(@filter_type).positive? %>
<% if logs&.any? && (display_delete_logs?(@current_user, searched, filter_type) || in_organisations_tab?) %> <br>
<%= govuk_link_to "Delete logs", delete_logs_path, class: "app-!-colour-red" %> <% end %>
<% end %> <% if logs&.any? && (display_delete_logs?(@current_user, searched, filter_type) || in_organisations_tab?) %>
</div> <%= govuk_link_to "Delete logs", delete_logs_path, class: "app-!-colour-red" %>
<% end %>
</div>
</div> </div>
</h2> </h2>
<% logs.map do |log| %> <% logs.map do |log| %>

6
app/views/logs/delete_duplicates.html.erb

@ -1,6 +1,6 @@
<% content_for :before_content do %> <% content_for :before_content do %>
<% content_for :title, "Are you sure you want to delete #{@duplicate_logs.count == 1 ? 'this duplicate log' : 'these duplicate logs'}?" %> <% content_for :title, "Are you sure you want to delete #{@duplicate_logs.count == 1 ? 'this duplicate log' : 'these duplicate logs'}?" %>
<%= govuk_back_link href: @log.lettings? ? lettings_log_duplicate_logs_path(@original_log, original_log_id: @original_log.id) : sales_log_duplicate_logs_path(@original_log, original_log_id: @original_log.id) %> <%= govuk_back_link href: @log.lettings? ? lettings_log_duplicate_logs_path(@original_log, original_log_id: @original_log.id, referrer: params[:referrer], organisation_id: params[:organisation_id]) : sales_log_duplicate_logs_path(@original_log, original_log_id: @original_log.id, referrer: params[:referrer], organisation_id: params[:organisation_id]) %>
<% end %> <% end %>
<div class="govuk-grid-row"> <div class="govuk-grid-row">
@ -29,10 +29,10 @@
<%= govuk_button_to @duplicate_logs.count == 1 ? "Delete this log" : "Delete these logs", <%= govuk_button_to @duplicate_logs.count == 1 ? "Delete this log" : "Delete these logs",
send("delete_logs_#{@log.class.name.underscore}s_path"), send("delete_logs_#{@log.class.name.underscore}s_path"),
method: "delete", method: "delete",
params: { ids: @duplicate_logs.map(&:id), original_log_id: @original_log.id, remaining_log_id: @log.id } %> params: { ids: @duplicate_logs.map(&:id), original_log_id: @original_log.id, remaining_log_id: @log.id, referrer: params[:referrer], organisation_id: params[:organisation_id] } %>
<%= govuk_button_link_to( <%= govuk_button_link_to(
"Cancel", "Cancel",
send("#{@log.class.name.underscore}_duplicate_logs_path", @original_log, original_log_id: @original_log.id), send("#{@log.class.name.underscore}_duplicate_logs_path", @original_log, original_log_id: @original_log.id, referrer: params[:referrer], organisation_id: params[:organisation_id]),
secondary: true, secondary: true,
) %> ) %>
</div> </div>

2
app/views/logs/download_csv.html.erb

@ -6,7 +6,7 @@
<div class="govuk-grid-row"> <div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds"> <div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-l">Download CSV</h2> <h1 class="govuk-heading-l">Download CSV</h1>
<p class="govuk-body">We'll send a secure download link to your email address <strong><%= @current_user.email %></strong>.</p> <p class="govuk-body">We'll send a secure download link to your email address <strong><%= @current_user.email %></strong>.</p>
<p class="govuk-body">You've selected <%= count %> logs.</p> <p class="govuk-body">You've selected <%= count %> logs.</p>

2
app/views/logs/index.html.erb

@ -9,7 +9,7 @@
) %> ) %>
<% if @duplicate_sets_count&.positive? %> <% if @duplicate_sets_count&.positive? %>
<%= govuk_notification_banner(title_text: "Important", text: govuk_link_to("Review logs", duplicate_logs_path)) do |banner| %> <%= govuk_notification_banner(title_text: "Important", text: govuk_link_to("Review logs", duplicate_logs_path(referrer: "duplicate_logs_banner"))) do |banner| %>
<% banner.with_heading(text: I18n.t("notification.duplicate_sets", count: @duplicate_sets_count)) %> <% banner.with_heading(text: I18n.t("notification.duplicate_sets", count: @duplicate_sets_count)) %>
<% end %> <% end %>
<% end %> <% end %>

11
app/views/maintenance/service_unavailable.html.erb

@ -0,0 +1,11 @@
<h1 class="govuk-heading-l govuk-!-width-two-thirds">
Sorry, the service is unavailable
</h1>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<p class="govuk-body">You will be able to use the service from 9am on Thursday 16 November 2023.</p>
<p class="govuk-body">Changes from the page you were on have not been saved. Changes on pages where you have selected 'save and continue' have been saved.</p>
<p class="govuk-body"><%= govuk_link_to "Contact the helpdesk", "https://dluhcdigital.atlassian.net/servicedesk/customer/portal/6/group/11" %> if you need help.</p>
</div>
</div>

2
app/views/organisation_relationships/_managing_agent_list.erb

@ -1,7 +1,7 @@
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>"> <section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "agents", path: request.path)) %> <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "agents", filters_count: 0)) %>
<% end %> <% end %>
<% @managing_agents.each do |managing_agent| %> <% @managing_agents.each do |managing_agent| %>
<%= table.body do |body| %> <%= table.body do |body| %>

2
app/views/organisation_relationships/_stock_owner_list.erb

@ -1,7 +1,7 @@
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>"> <section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "stock owners", path: request.path)) %> <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "stock owners", filters_count: 0)) %>
<% end %> <% end %>
<% @stock_owners.each do |stock_owner| %> <% @stock_owners.each do |stock_owner| %>
<%= table.body do |body| %> <%= table.body do |body| %>

34
app/views/organisations/_merged_organisation_details.html.erb

@ -0,0 +1,34 @@
<% if @organisation.recently_absorbed_organisations_grouped_by_merge_date.present? %>
<%= govuk_details(summary_text: "View all organisations that were merged into #{@organisation.name}") do %>
<% @organisation.recently_absorbed_organisations_grouped_by_merge_date.each do |merge_date, organisations| %>
<p><strong>Merge date:</strong> <%= merge_date&.to_formatted_s(:govuk_date) %></p>
<%= govuk_table do |table| %>
<%= table.head do |head| %>
<%= head.row do |row| %>
<% row.cell(header: true, text: "Organisation name", html_attributes: { scope: "col", class: "govuk-!-width-one-half" }) %>
<% row.cell(header: true, text: "Organisation ID", html_attributes: { scope: "col", class: "govuk-!-width-one-half" }) %>
<% end %>
<% end %>
<% organisations.each do |absorbed_org| %>
<%= table.body do |body| %>
<%= body.row do |row| %>
<% if current_user.support? %>
<% row.cell(text: simple_format(govuk_link_to(absorbed_org.name, organisation_path(absorbed_org)), { class: "govuk-!-font-weight-bold scheme-name-cell" }, wrapper_tag: "div")) %>
<% else %>
<% row.cell(text: absorbed_org.name) %>
<% end %>
<% row.cell(text: "ORG#{absorbed_org.id}") %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% if @organisation.absorbing_organisation.present? %>
<% if current_user.support? %>
<p><%= @organisation.name %> was merged into <%= govuk_link_to(@organisation.absorbing_organisation.name, organisation_path(@organisation.absorbing_organisation)) %><%= @organisation.merge_date ? " on #{@organisation.merge_date.to_formatted_s(:govuk_date)}" : "" %>.</p>
<% else %>
<p><%= @organisation.name %> was merged into <%= @organisation.absorbing_organisation.name %><%= @organisation.merge_date ? " on #{@organisation.merge_date.to_formatted_s(:govuk_date)}" : "" %>.</p>
<% end %>
<% end %>

2
app/views/organisations/_organisation_list.html.erb

@ -1,7 +1,7 @@
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>"> <section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "organisations", path: request.path)) %> <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "organisations", filters_count: applied_filters_count(@filter_type))) %>
<% end %> <% end %>
<%= table.head do |head| %> <%= table.head do |head| %>
<%= head.row do |row| %> <%= head.row do |row| %>

2
app/views/organisations/logs.html.erb

@ -11,7 +11,7 @@
) %> ) %>
<% if @duplicate_sets_count&.positive? %> <% if @duplicate_sets_count&.positive? %>
<%= govuk_notification_banner(title_text: "Important", text: govuk_link_to("Review logs", organisation_duplicates_path(@organisation))) do |banner| %> <%= govuk_notification_banner(title_text: "Important", text: govuk_link_to("Review logs", organisation_duplicates_path(@organisation, referrer: "duplicate_logs_banner"))) do |banner| %>
<% banner.with_heading(text: I18n.t("notification.duplicate_sets", count: @duplicate_sets_count)) %> <% banner.with_heading(text: I18n.t("notification.duplicate_sets", count: @duplicate_sets_count)) %>
<% end %> <% end %>
<% end %> <% end %>

1
app/views/organisations/show.html.erb

@ -39,6 +39,7 @@
<% if FeatureToggle.merge_organisations_enabled? %> <% if FeatureToggle.merge_organisations_enabled? %>
<p>To report a merge or update your organisation details, <%= govuk_link_to "contact the helpdesk", "https://dluhcdigital.atlassian.net/servicedesk/customer/portal/6/group/11" %>.</p> <p>To report a merge or update your organisation details, <%= govuk_link_to "contact the helpdesk", "https://dluhcdigital.atlassian.net/servicedesk/customer/portal/6/group/11" %>.</p>
<% end %> <% end %>
<%= render partial: "organisations/merged_organisation_details" %>
</div> </div>
<div class="govuk-grid-column-one-third-from-desktop"> <div class="govuk-grid-column-one-third-from-desktop">

6
app/views/schemes/_scheme_filters.html.erb

@ -11,7 +11,7 @@
<%= filters_applied_text(@filter_type) %> <%= filters_applied_text(@filter_type) %>
</p> </p>
<p class="govuk-!-text-align-right govuk-grid-column-one-half"> <p class="govuk-!-text-align-right govuk-grid-column-one-half">
<%= reset_filters_link(@filter_type) %> <%= reset_filters_link(@filter_type, { search: request.params["search"] }.compact) %>
</p> </p>
</div> </div>
@ -43,6 +43,10 @@
} %> } %>
<% end %> <% end %>
<% if request.params["search"].present? %>
<%= f.hidden_field :search, value: request.params["search"] %>
<% end %>
<%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %>
<% end %> <% end %>
</div> </div>

2
app/views/schemes/_scheme_list.html.erb

@ -1,7 +1,7 @@
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>"> <section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "schemes", path: request.path)) %> <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "schemes", filters_count: applied_filters_count(@filter_type))) %>
<% end %> <% end %>
<%= table.head do |head| %> <%= table.head do |head| %>
<%= head.row do |row| %> <%= head.row do |row| %>

2
app/views/schemes/support.html.erb

@ -15,7 +15,7 @@
<% support_level_options_hints = { "Low level": "Staff visiting once a week, fortnightly or less.", "Medium level": "Staff on site daily or making frequent visits with some out-of-hours cover.", "High level": "Intensive level of staffing provided on a 24-hour basis." } %> <% support_level_options_hints = { "Low level": "Staff visiting once a week, fortnightly or less.", "Medium level": "Staff on site daily or making frequent visits with some out-of-hours cover.", "High level": "Intensive level of staffing provided on a 24-hour basis." } %>
<% support_level_options_with_hints = Scheme.support_types.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize, description: support_level_options_hints[key.to_sym]) } %> <% support_level_options_with_hints = Scheme.support_types.keys.excluding("Missing").excluding("Floating support").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize, description: support_level_options_hints[key.to_sym]) } %>
<%= f.govuk_collection_radio_buttons :support_type, <%= f.govuk_collection_radio_buttons :support_type,
support_level_options_with_hints, support_level_options_with_hints,

7
app/views/users/_user_filters.html.erb

@ -11,7 +11,7 @@
<%= filters_applied_text(@filter_type) %> <%= filters_applied_text(@filter_type) %>
</p> </p>
<p class="govuk-!-text-align-right govuk-grid-column-one-half"> <p class="govuk-!-text-align-right govuk-grid-column-one-half">
<%= reset_filters_link(@filter_type) %> <%= reset_filters_link(@filter_type, { search: request.params["search"] }.compact) %>
</p> </p>
</div> </div>
@ -22,6 +22,11 @@
label: "Status", label: "Status",
category: "status", category: "status",
} %> } %>
<% if request.params["search"].present? %>
<%= f.hidden_field :search, value: request.params["search"] %>
<% end %>
<%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %>
<% end %> <% end %>
</div> </div>

2
app/views/users/_user_list.html.erb

@ -1,7 +1,7 @@
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>"> <section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "users", path: request.path)) %> <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "users", filters_count: applied_filters_count(@filter_type))) %>
<% if current_user.support? %> <% if current_user.support? %>
<% query = searched.present? ? "?search=#{searched}" : nil %> <% query = searched.present? ? "?search=#{searched}" : nil %>
<%= govuk_link_to "Download (CSV)", "#{request.path}.csv#{query}", type: "text/csv" %> <%= govuk_link_to "Download (CSV)", "#{request.path}.csv#{query}", type: "text/csv" %>

6
app/views/users/new.html.erb

@ -4,7 +4,7 @@
<%= govuk_back_link(href: :back) %> <%= govuk_back_link(href: :back) %>
<% end %> <% end %>
<%= form_for(@resource, as: :user, html: { method: :post }) do |f| %> <%= form_for(@user, as: :user, html: { method: :post }) do |f| %>
<div class="govuk-grid-row"> <div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds"> <div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %> <%= f.govuk_error_summary %>
@ -21,13 +21,13 @@
label: { text: "Email address", size: "m" }, label: { text: "Email address", size: "m" },
autocomplete: "email", autocomplete: "email",
spellcheck: "false", spellcheck: "false",
value: @resource.email %> value: @user.email %>
<%= f.govuk_phone_field :phone, <%= f.govuk_phone_field :phone,
label: { text: "Telephone number", size: "m" }, label: { text: "Telephone number", size: "m" },
autocomplete: "phone", autocomplete: "phone",
spellcheck: "false", spellcheck: "false",
value: @resource.phone %> value: @user.phone %>
<% if current_user.support? %> <% if current_user.support? %>
<% null_option = [OpenStruct.new(id: "", name: "Select an option")] %> <% null_option = [OpenStruct.new(id: "", name: "Select an option")] %>

12
config/forms/2021_2022.json

@ -7074,7 +7074,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no referral or nomination)" "value": "Tenant applied directly (no referral or nomination)"
@ -7131,7 +7132,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no nomination)" "value": "Tenant applied directly (no nomination)"
@ -7194,7 +7196,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no referral)" "value": "Tenant applied directly (no referral)"
@ -7254,7 +7257,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no referral or nomination)" "value": "Tenant applied directly (no referral or nomination)"

20
config/forms/2022_2023.json

@ -6991,7 +6991,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no referral or nomination)" "value": "Tenant applied directly (no referral or nomination)"
@ -7048,7 +7049,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no nomination)" "value": "Tenant applied directly (no nomination)"
@ -7056,8 +7058,8 @@
"3": { "3": {
"value": "Nominated by a local housing authority" "value": "Nominated by a local housing authority"
}, },
"4" : { "4" : {
"value" : "Referred by local authority housing department" "value" : "Referred by local authority housing department"
}, },
"8": { "8": {
"value": "Re-located through official housing mobility scheme" "value": "Re-located through official housing mobility scheme"
@ -7111,7 +7113,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no referral)" "value": "Tenant applied directly (no referral)"
@ -7171,7 +7174,8 @@
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
"1": { "1": {
"value": "Internal transfer" "value": "Internal transfer",
"hint": "Where the tenant has moved to another social property owned by the same landlord."
}, },
"2": { "2": {
"value": "Tenant applied directly (no referral or nomination)" "value": "Tenant applied directly (no referral or nomination)"
@ -8316,7 +8320,7 @@
} }
} }
}, },
"interruption_screen_question_ids": ["brent", "startdate", "la", "beds", "rent_type", "needstype"] "interruption_screen_question_ids": ["brent", "period", "startdate", "la", "beds", "rent_type", "needstype"]
}, },
"max_rent_value_check": { "max_rent_value_check": {
"depends_on": [ "depends_on": [
@ -8361,7 +8365,7 @@
} }
} }
}, },
"interruption_screen_question_ids": ["brent", "startdate", "la", "beds", "rent_type", "needstype"] "interruption_screen_question_ids": ["brent", "period", "startdate", "la", "beds", "rent_type", "needstype"]
}, },
"scharge_value_check": { "scharge_value_check": {
"depends_on": [ "depends_on": [

5
config/initializers/sidekiq.rb

@ -32,7 +32,10 @@ Redis.silence_deprecations = true
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.on(:startup) do config.on(:startup) do
Sidekiq::Cron::Job.load_from_hash YAML.load_file("config/sidekiq_cron_schedule.yml") Sidekiq::Cron::Job.all.each(&:destroy)
unless FeatureToggle.maintenance_mode_enabled?
Sidekiq::Cron::Job.load_from_hash YAML.load_file("config/sidekiq_cron_schedule.yml")
end
end end
config.on(:shutdown) do config.on(:shutdown) do

26
config/locales/en.yml

@ -265,7 +265,7 @@ en:
owning_organisation: "Enter a date when the owning organisation was active. %{owning_organisation} became inactive on %{owning_organisation_merge_date} and was replaced by %{owning_absorbing_organisation}." owning_organisation: "Enter a date when the owning organisation was active. %{owning_organisation} became inactive on %{owning_organisation_merge_date} and was replaced by %{owning_absorbing_organisation}."
managing_organisation: "Enter a date when the managing organisation was active. %{managing_organisation} became inactive on %{managing_organisation_merge_date} and was replaced by %{managing_absorbing_organisation}." managing_organisation: "Enter a date when the managing organisation was active. %{managing_organisation} became inactive on %{managing_organisation_merge_date} and was replaced by %{managing_absorbing_organisation}."
different_merge: "Enter a date when the owning and managing organisations were active. %{owning_organisation} became inactive on %{owning_organisation_merge_date} and was replaced by %{owning_absorbing_organisation}. %{managing_organisation} became inactive on %{managing_organisation_merge_date} and was replaced by %{managing_absorbing_organisation}." different_merge: "Enter a date when the owning and managing organisations were active. %{owning_organisation} became inactive on %{owning_organisation_merge_date} and was replaced by %{owning_absorbing_organisation}. %{managing_organisation} became inactive on %{managing_organisation_merge_date} and was replaced by %{managing_absorbing_organisation}."
invalid_absorbing_organisations_start_date: invalid_absorbing_organisations_start_date:
same_organisation: "Enter a date when the owning and managing organisation was active. %{owning_organisation} became active on %{owning_organisation_available_from}." same_organisation: "Enter a date when the owning and managing organisation was active. %{owning_organisation} became active on %{owning_organisation_available_from}."
owning_organisation: "Enter a date when the owning organisation was active. %{owning_organisation} became active on %{owning_organisation_available_from}." owning_organisation: "Enter a date when the owning organisation was active. %{owning_organisation} became active on %{owning_organisation_available_from}."
managing_organisation: "Enter a date when the managing organisation was active. %{managing_organisation} became active on %{managing_organisation_available_from}." managing_organisation: "Enter a date when the managing organisation was active. %{managing_organisation} became active on %{managing_organisation_available_from}."
@ -367,27 +367,7 @@ en:
negative_currency: "Enter an amount above 0" negative_currency: "Enter an amount above 0"
rent: rent:
less_than_shortfall: "Enter an amount that is more than the shortfall in basic rent" less_than_shortfall: "Enter an amount that is more than the shortfall in basic rent"
scharge: out_of_range: "Enter a value for the %{charge_name} between £0 and %{maximum_per_period} paid %{frequency}. %{maximum_per_period} is the max limit for rent and charges paid %{frequency} for %{letting_type} lettings owned by a %{provider_type}."
private_registered_provider:
general_needs: "Enter a value for the service charge between £0 and £800 per week if the landlord is a private registered provider and it is a general needs letting"
supported_housing: "Enter a value for the service charge between £0 and £800 per week if the landlord is a private registered provider and it is a supported housing letting"
local_authority:
general_needs: "Enter a value for the service charge between £0 and £500 per week if the landlord is a local authority and it is a general needs letting"
supported_housing: "Enter a value for the service charge between £0 and £500 per week if the landlord is a local authority and it is a supported housing letting"
pscharge:
private_registered_provider:
general_needs: "Enter a value for the personal service charge between £0 and £700 per week if the landlord is a private registered provider and it is a general needs letting"
supported_housing: "Enter a value for the personal service charge between £0 and £700 per week if the landlord is a private registered provider and it is a supported housing letting"
local_authority:
general_needs: "Enter a value for the personal service charge between £0 and £200 per week if the landlord is a local authority and it is a general needs letting"
supported_housing: "Enter a value for the personal service charge between £0 and £200 per week if the landlord is a local authority and it is a supported housing letting"
supcharg:
private_registered_provider:
general_needs: "Enter a value for the support charge between £0 and £800 per week if the landlord is a private registered provider and it is a general needs letting"
supported_housing: "Enter a value for the support charge between £0 and £800 per week if the landlord is a private registered provider and it is a supported housing letting"
local_authority:
general_needs: "Enter a value for the support charge between £0 and £200 per week if the landlord is a local authority and it is a general needs letting"
supported_housing: "Enter a value for the support charge between £0 and £200 per week if the landlord is a local authority and it is a supported housing letting"
ecstat: ecstat:
over_hard_max: "Net income of %{hard_max} per week is too high given the tenant’s working situation" over_hard_max: "Net income of %{hard_max} per week is too high given the tenant’s working situation"
brent: brent:
@ -722,7 +702,7 @@ Make sure these answers are correct."
charges: charges:
informative_text: "This is higher than we would expect." informative_text: "This is higher than we would expect."
hint_text: "Check the following:<ul class=\"govuk-body-l app-panel--interruption\"><li>the decimal point</li><li>the frequency, for example every week or every calendar month</li><li>the needs type</li></ul>" hint_text: "Check the following:<ul class=\"govuk-body-l app-panel--interruption\"><li>the decimal point</li><li>the frequency, for example every week or every calendar month</li><li>the needs type</li></ul>"
devise: devise:
email: email:

1
config/routes.rb

@ -35,6 +35,7 @@ Rails.application.routes.draw do
get "/accessibility-statement", to: "content#accessibility_statement" get "/accessibility-statement", to: "content#accessibility_statement"
get "/privacy-notice", to: "content#privacy_notice" get "/privacy-notice", to: "content#privacy_notice"
get "/data-sharing-agreement", to: "content#data_sharing_agreement" get "/data-sharing-agreement", to: "content#data_sharing_agreement"
get "/service-unavailable", to: "maintenance#service_unavailable"
get "/download-23-24-lettings-form", to: "start#download_23_24_lettings_form" get "/download-23-24-lettings-form", to: "start#download_23_24_lettings_form"
get "/download-22-23-lettings-form", to: "start#download_22_23_lettings_form" get "/download-22-23-lettings-form", to: "start#download_22_23_lettings_form"

5
db/migrate/20231023142854_add_available_from_to_org.rb

@ -0,0 +1,5 @@
class AddAvailableFromToOrg < ActiveRecord::Migration[7.0]
def change
add_column :organisations, :available_from, :datetime
end
end

3
db/schema.rb

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_09_13_093443) do ActiveRecord::Schema[7.0].define(version: 2023_10_23_142854) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -440,6 +440,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_13_093443) do
t.string "old_visible_id" t.string "old_visible_id"
t.datetime "merge_date" t.datetime "merge_date"
t.bigint "absorbing_organisation_id" t.bigint "absorbing_organisation_id"
t.datetime "available_from"
t.index ["absorbing_organisation_id"], name: "index_organisations_on_absorbing_organisation_id" t.index ["absorbing_organisation_id"], name: "index_organisations_on_absorbing_organisation_id"
t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true
end end

2
docs/adr/index.md

@ -1,6 +1,6 @@
--- ---
has_children: true has_children: true
nav_order: 9 nav_order: 10
--- ---
# Architecture decisions # Architecture decisions

15
docs/app_api.md

@ -0,0 +1,15 @@
---
nav_order: 8
---
# Using the App API
In order to use the app as an API, you will need to configure requests to the API as so:
* Configure your request with Basic Auth. Set the username to be the same as `API_USER` and password as the `API_KEY` (`API_USER` and `API_KEY` are environment variables that should be set for the application)
* Check the endpoint you are calling is an action that is `create`, `show` or `update`
* Check you are setting the following request headers:
* `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.

2
docs/documentation_website.md

@ -1,5 +1,5 @@
--- ---
nav_order: 10 nav_order: 11
--- ---
# This documentation website # This documentation website

2
docs/form/index.md

@ -1,6 +1,6 @@
--- ---
has_children: true has_children: true
nav_order: 8 nav_order: 9
--- ---
# Generating forms # Generating forms

2
docs/setup.md

@ -1,5 +1,5 @@
--- ---
nav_order: 2 nav_order: 3
--- ---
# Local development # Local development

4
lib/tasks/correct_illness_from_csv.rake

@ -20,7 +20,9 @@ namespace :correct_illness do
raise "Usage: rake correct_illness:correct_illness_from_csv['csv_file_name']" if file_name.blank? raise "Usage: rake correct_illness:correct_illness_from_csv['csv_file_name']" if file_name.blank?
s3_service = Storage::S3Service.new(PlatformHelper.is_paas? ? Configuration::PaasConfigurationService.new : Configuration::EnvConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"]) s3_service = Storage::S3Service.new(PlatformHelper.is_paas? ? Configuration::PaasConfigurationService.new : Configuration::EnvConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"])
illness_csv = CSV.parse(s3_service.get_file_io(file_name), headers: false) file_io = s3_service.get_file_io(file_name)
file_io.set_encoding_by_bom
illness_csv = CSV.parse(file_io, headers: false)
illness_csv.each_with_index do |row, index| illness_csv.each_with_index do |row, index|
next if index < 3 next if index < 3

22
lib/tasks/import_address_from_csv.rake

@ -6,7 +6,9 @@ namespace :data_import do
raise "Usage: rake data_import:import_lettings_addresses_from_csv['csv_file_name']" if file_name.blank? raise "Usage: rake data_import:import_lettings_addresses_from_csv['csv_file_name']" if file_name.blank?
s3_service = Storage::S3Service.new(PlatformHelper.is_paas? ? Configuration::PaasConfigurationService.new : Configuration::EnvConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"]) s3_service = Storage::S3Service.new(PlatformHelper.is_paas? ? Configuration::PaasConfigurationService.new : Configuration::EnvConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"])
addresses_csv = CSV.parse(s3_service.get_file_io(file_name), headers: true) file_io = s3_service.get_file_io(file_name)
file_io.set_encoding_by_bom
addresses_csv = CSV.parse(file_io, headers: true)
contains_issue_type = addresses_csv.headers.include?("Issue type") contains_issue_type = addresses_csv.headers.include?("Issue type")
addresses_csv.each do |row| addresses_csv.each do |row|
@ -53,8 +55,11 @@ namespace :data_import do
lettings_log.send("process_postcode_changes!") lettings_log.send("process_postcode_changes!")
lettings_log.values_updated_at = Time.zone.now lettings_log.values_updated_at = Time.zone.now
lettings_log.save! if lettings_log.save
Rails.logger.info("Updated lettings log #{lettings_log_id}, with address: #{[lettings_log.address_line1, lettings_log.address_line2, lettings_log.town_or_city, lettings_log.county, lettings_log.postcode_full].join(', ')}") Rails.logger.info("Updated lettings log #{lettings_log_id}, with address: #{[lettings_log.address_line1, lettings_log.address_line2, lettings_log.town_or_city, lettings_log.county, lettings_log.postcode_full].join(', ')}")
else
Rails.logger.error("Validation failed for lettings log with ID #{lettings_log.id}: #{lettings_log.errors.full_messages.join(', ')}}")
end
end end
end end
@ -65,7 +70,9 @@ namespace :data_import do
raise "Usage: rake data_import:import_sales_addresses_from_csv['csv_file_name']" if file_name.blank? raise "Usage: rake data_import:import_sales_addresses_from_csv['csv_file_name']" if file_name.blank?
s3_service = Storage::S3Service.new(PlatformHelper.is_paas? ? Configuration::PaasConfigurationService.new : Configuration::EnvConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"]) s3_service = Storage::S3Service.new(PlatformHelper.is_paas? ? Configuration::PaasConfigurationService.new : Configuration::EnvConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"])
addresses_csv = CSV.parse(s3_service.get_file_io(file_name), headers: true) file_io = s3_service.get_file_io(file_name)
file_io.set_encoding_by_bom
addresses_csv = CSV.parse(file_io, headers: true)
contains_issue_type = addresses_csv.headers.include?("Issue type") contains_issue_type = addresses_csv.headers.include?("Issue type")
addresses_csv.each do |row| addresses_csv.each do |row|
@ -112,8 +119,11 @@ namespace :data_import do
sales_log.send("process_postcode_changes!") sales_log.send("process_postcode_changes!")
sales_log.values_updated_at = Time.zone.now sales_log.values_updated_at = Time.zone.now
sales_log.save! if sales_log.save
Rails.logger.info("Updated sales log #{sales_log_id}, with address: #{[sales_log.address_line1, sales_log.address_line2, sales_log.town_or_city, sales_log.county, sales_log.postcode_full].join(', ')}") Rails.logger.info("Updated sales log #{sales_log_id}, with address: #{[sales_log.address_line1, sales_log.address_line2, sales_log.town_or_city, sales_log.county, sales_log.postcode_full].join(', ')}")
else
Rails.logger.error("Validation failed for sales log with ID #{sales_log.id}: #{sales_log.errors.full_messages.join(', ')}}")
end
end end
end end
end end

33
lib/tasks/merge_organisations.rake

@ -1,12 +1,37 @@
namespace :merge do namespace :merge do
desc "Merge organisations into one" desc "Merge organisations into an existing organisation"
task :merge_organisations, %i[absorbing_organisation_id merging_organisation_ids] => :environment do |_task, args| task :merge_organisations, %i[absorbing_organisation_id merging_organisation_ids merge_date] => :environment do |_task, args|
absorbing_organisation_id = args[:absorbing_organisation_id] absorbing_organisation_id = args[:absorbing_organisation_id]
merging_organisation_ids = args[:merging_organisation_ids]&.split(" ")&.map(&:to_i) merging_organisation_ids = args[:merging_organisation_ids]&.split(" ")&.map(&:to_i)
begin
merge_date = args[:merge_date].present? ? Date.parse(args[:merge_date]) : nil
rescue StandardError
raise "Usage: rake merge:merge_organisations[absorbing_organisation_id, merging_organisation_ids, merge_date]. Merge date must be in format YYYY-MM-DD"
end
raise "Usage: rake merge:merge_organisations[absorbing_organisation_id, merging_organisation_ids]" if merging_organisation_ids.blank? || absorbing_organisation_id.blank? if merging_organisation_ids.blank? || absorbing_organisation_id.blank?
raise "Usage: rake merge:merge_organisations[absorbing_organisation_id, merging_organisation_ids, merge_date]"
end
service = Merge::MergeOrganisationsService.new(absorbing_organisation_id:, merging_organisation_ids:) service = Merge::MergeOrganisationsService.new(absorbing_organisation_id:, merging_organisation_ids:, merge_date:)
service.call
end
desc "Merge organisations into an existing organisation, make the absorbing organisation active from merge date only"
task :merge_organisations_into_new_organisation, %i[absorbing_organisation_id merging_organisation_ids merge_date] => :environment do |_task, args|
absorbing_organisation_id = args[:absorbing_organisation_id]
merging_organisation_ids = args[:merging_organisation_ids]&.split(" ")&.map(&:to_i)
begin
merge_date = args[:merge_date].present? ? Date.parse(args[:merge_date]) : nil
rescue StandardError
raise "Usage: rake merge:merge_organisations_into_new_organisation[absorbing_organisation_id, merging_organisation_ids, merge_date]. Merge date must be in format YYYY-MM-DD"
end
if merging_organisation_ids.blank? || absorbing_organisation_id.blank?
raise "Usage: rake merge:merge_organisations_into_new_organisation[absorbing_organisation_id, merging_organisation_ids, merge_date]"
end
service = Merge::MergeOrganisationsService.new(absorbing_organisation_id:, merging_organisation_ids:, merge_date:, absorbing_organisation_active_from_merge_date: true)
service.call service.call
end end
end end

18
lib/tasks/recalculate_irproduct_values.rake

@ -0,0 +1,18 @@
desc "Forces to recalculate irproduct values"
task recalculate_irproduct_values: :environment do
LettingsLog.exportable.where(rent_type: [3, 4, 5]).where(irproduct: nil).each do |log| # irproduct was never set
Rails.logger.info("Could not update irproduct for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
LettingsLog.exportable.where(rent_type: 3).where.not(irproduct: 1).each do |log| # irproduct was set wrong
Rails.logger.info("Could not update irproduct for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
LettingsLog.exportable.where(rent_type: 4).where.not(irproduct: 2).each do |log| # irproduct was set wrong
Rails.logger.info("Could not update irproduct for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
LettingsLog.exportable.where(rent_type: 5).where.not(irproduct: 3).each do |log| # irproduct was set wrong
Rails.logger.info("Could not update irproduct for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
LettingsLog.exportable.where.not(rent_type: [3, 4, 5]).where.not(irproduct: nil).each do |log| # irproduct was set when it should have been nil
Rails.logger.info("Could not update irproduct for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
end

13
lib/tasks/recalculate_lar_values.rake

@ -0,0 +1,13 @@
desc "Forces to recalculate lar values for affordable rent types and clears irrelevant lar values"
task recalculate_lar_values: :environment do
LettingsLog.exportable.where(rent_type: [1, 2], lar: nil).each do |log| # lar was never set
Rails.logger.info("Could not update lar for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
LettingsLog.exportable.where(rent_type: 1).where.not(lar: 2).each do |log| # lar was set wrong
Rails.logger.info("Could not update lar for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
LettingsLog.exportable.where(rent_type: 2).where.not(lar: 1).each do |log| # lar was set wrong
Rails.logger.info("Could not update lar for LettingsLog #{log.id}") unless log.update(values_updated_at: Time.zone.now)
end
LettingsLog.exportable.where.not(rent_type: [1, 2]).where.not(lar: nil).update_all(lar: nil) # lar was set to 2 but should never have been set
end

35
lib/tasks/squish_names.rake

@ -0,0 +1,35 @@
desc "Squish names of locations, schemes, users, and organisations"
task squish_names: :environment do
Location.where("name LIKE ?", "% %").each do |location|
location.name&.squish!
begin
location.save!
rescue StandardError => e
Sentry.capture_exception(e)
end
end
Scheme.where("service_name LIKE ?", "% %").each do |scheme|
scheme.service_name&.squish!
begin
scheme.save!
rescue StandardError => e
Sentry.capture_exception(e)
end
end
User.where("name LIKE ?", "% %").each do |user|
user.name&.squish!
begin
user.save!
rescue StandardError => e
Sentry.capture_exception(e)
end
end
Organisation.where("name LIKE ?", "% %").each do |organisation|
organisation.name&.squish!
begin
organisation.save!
rescue StandardError => e
Sentry.capture_exception(e)
end
end
end

47
spec/components/search_result_caption_component_spec.rb

@ -6,20 +6,53 @@ RSpec.describe SearchResultCaptionComponent, type: :component do
let(:item_label) { "user" } let(:item_label) { "user" }
let(:total_count) { 3 } let(:total_count) { 3 }
let(:item) { "schemes" } let(:item) { "schemes" }
let(:path) { "path" } let(:filters_count) { 1 }
let(:result) { render_inline(described_class.new(searched:, count:, item_label:, total_count:, item:, filters_count:)) }
it "renders table caption including the search results and total" do context "when search and filter results are found" do
result = render_inline(described_class.new(searched:, count:, item_label:, total_count:, item:, path:)) it "renders table caption including the search results and total" do
expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>#{count}</strong> #{item_label} found matching ‘#{searched}’ of <strong>#{total_count}</strong> total #{item}. <a class=\"govuk-link\" href=\"path\">Clear search</a>\n</span>\n") expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>2</strong> users matching search and filters<br>\n</span>\n")
end
end
context "when search results are found" do
let(:filters_count) { nil }
it "renders table caption including the search results and total" do
expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>2</strong> users matching search<br>\n</span>\n")
end
end
context "when filter results are found" do
let(:searched) { nil }
it "renders table caption including the search results and total" do
expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>2</strong> users matching filters<br>\n</span>\n")
end
end end
context "when no search results are found" do context "when no search/filter is applied" do
let(:searched) { nil } let(:searched) { nil }
let(:filters_count) { nil }
it "renders table caption with total count only" do it "renders table caption with total count only" do
result = render_inline(described_class.new(searched:, count:, item_label:, total_count:, item:, path:)) expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>#{count}</strong> matching #{item}\n</span>\n")
end
end
context "when nothing is found" do
let(:count) { 0 }
expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>#{count}</strong> total #{item}\n</span>\n") it "renders table caption with total count only" do
expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>0</strong> users matching search and filters<br>\n</span>\n")
end
end
context "when 1 record is found" do
let(:count) { 1 }
it "renders table caption with total count only" do
expect(result.to_html).to eq("<span class=\"govuk-!-margin-right-4\">\n <strong>1</strong> user matching search and filters<br>\n</span>\n")
end end
end end
end end

27
spec/controllers/maintenance_controller_spec.rb

@ -0,0 +1,27 @@
require "rails_helper"
RSpec.describe MaintenanceController do
let(:user) { FactoryBot.create(:user) }
describe "GET #service_unavailable" do
context "when maintenance mode is enabled" do
it "logs the user out" do
allow(FeatureToggle).to receive(:maintenance_mode_enabled?).and_return(true)
sign_in user
expect(controller).to be_user_signed_in
get :service_unavailable
expect(controller).not_to be_user_signed_in
end
end
context "when maintenance mode is disabled" do
it "doesn't log the user out" do
allow(FeatureToggle).to receive(:maintenance_mode_enabled?).and_return(false)
sign_in user
expect(controller).to be_user_signed_in
get :service_unavailable
expect(controller).to be_user_signed_in
end
end
end
end

5
spec/factories/lettings_log.rb

@ -3,7 +3,10 @@ FactoryBot.define do
created_by { FactoryBot.create(:user) } created_by { FactoryBot.create(:user) }
owning_organisation { created_by.organisation } owning_organisation { created_by.organisation }
managing_organisation { created_by.organisation } managing_organisation { created_by.organisation }
created_at { Time.zone.today }
updated_at { Time.zone.today }
trait :setup_completed do trait :setup_completed do
startdate_today
renewal { 0 } renewal { 0 }
needstype { 1 } needstype { 1 }
rent_type { 1 } rent_type { 1 }
@ -206,7 +209,5 @@ FactoryBot.define do
illness_type_9 { false } illness_type_9 { false }
illness_type_10 { false } illness_type_10 { false }
end end
created_at { Time.zone.today }
updated_at { Time.zone.today }
end end
end end

1
spec/factories/user.rb

@ -5,6 +5,7 @@ FactoryBot.define do
password { "pAssword1" } password { "pAssword1" }
organisation organisation
role { "data_provider" } role { "data_provider" }
phone { "1234512345123" }
trait :data_coordinator do trait :data_coordinator do
role { "data_coordinator" } role { "data_coordinator" }
end end

14
spec/features/form/form_navigation_spec.rb

@ -176,4 +176,18 @@ RSpec.describe "Form Navigation" do
end end
end end
end end
describe "fixing duplicate logs" do
it "shows a correct cancel link" do
visit("lettings-logs/#{id}/tenant-code-test?first_remaining_duplicate_id=x&original_log_id=#{id}&referrer=duplicate_logs")
click_link(text: "Cancel")
expect(page).to have_current_path("/lettings-logs/#{id}/duplicate-logs?original_log_id=#{id}")
end
it "shows a correct Save Changes buttons" do
visit("lettings-logs/#{id}/tenant-code-test?first_remaining_duplicate_id=#{id}&original_log_id=#{id}&referrer=duplicate_logs")
click_button(text: "Save changes")
expect(page).to have_current_path("/lettings-logs/#{id}/duplicate-logs?original_log_id=#{id}&referrer=duplicate_logs")
end
end
end end

18
spec/features/lettings_log_spec.rb

@ -79,7 +79,7 @@ RSpec.describe "Lettings Log Features" do
context "when no filters are selected" do context "when no filters are selected" do
it "displays the filters component with no clear button" do it "displays the filters component with no clear button" do
expect(page).to have_content("No filters applied") expect(page).to have_content("No filters applied")
expect(page).not_to have_content("Clear") expect(page).not_to have_link("Clear", href: "/clear-filters?filter_type=lettings_logs")
end end
end end
@ -97,7 +97,7 @@ RSpec.describe "Lettings Log Features" do
it "displays the filters component with a correct count and clear button" do it "displays the filters component with a correct count and clear button" do
expect(page).to have_content("5 filters applied") expect(page).to have_content("5 filters applied")
expect(page).to have_content("Clear") expect(page).to have_link("Clear", href: "/clear-filters?filter_type=lettings_logs")
end end
context "when clearing the filters" do context "when clearing the filters" do
@ -107,7 +107,7 @@ RSpec.describe "Lettings Log Features" do
it "clears the filters and displays the filter component as before" do it "clears the filters and displays the filter component as before" do
expect(page).to have_content("No filters applied") expect(page).to have_content("No filters applied")
expect(page).not_to have_content("Clear") expect(page).not_to have_link("Clear", href: "/clear-filters?filter_type=lettings_logs")
end end
end end
end end
@ -467,7 +467,7 @@ RSpec.describe "Lettings Log Features" do
expect(duplicate_log.deleted?).to be true expect(duplicate_log.deleted?).to be true
expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success")
expect(page).to have_content("Log #{duplicate_log.id} has been deleted.") expect(page).to have_content("Log #{duplicate_log.id} has been deleted.")
expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}") expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/duplicate-logs?organisation_id=&original_log_id=#{lettings_log.id}&referrer=")
expect(page).not_to have_content("These logs are duplicates") expect(page).not_to have_content("These logs are duplicates")
expect(page).not_to have_link("Keep this log and delete duplicates") expect(page).not_to have_link("Keep this log and delete duplicates")
expect(page).to have_link("Back to Log #{lettings_log.id}", href: "/lettings-logs/#{lettings_log.id}") expect(page).to have_link("Back to Log #{lettings_log.id}", href: "/lettings-logs/#{lettings_log.id}")
@ -491,7 +491,7 @@ RSpec.describe "Lettings Log Features" do
expect(lettings_log.status).to eq("deleted") expect(lettings_log.status).to eq("deleted")
expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success")
expect(page).to have_content("Log #{lettings_log.id} has been deleted.") expect(page).to have_content("Log #{lettings_log.id} has been deleted.")
expect(page).to have_current_path("/lettings-logs/#{duplicate_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}") expect(page).to have_current_path("/lettings-logs/#{duplicate_log.id}/duplicate-logs?organisation_id=&original_log_id=#{lettings_log.id}&referrer=")
expect(page).not_to have_content("These logs are duplicates") expect(page).not_to have_content("These logs are duplicates")
expect(page).not_to have_link("Keep this log and delete duplicates") expect(page).not_to have_link("Keep this log and delete duplicates")
expect(page).to have_link("Back to lettings logs", href: "/lettings-logs") expect(page).to have_link("Back to lettings logs", href: "/lettings-logs")
@ -510,8 +510,8 @@ RSpec.describe "Lettings Log Features" do
expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}") expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}")
click_link("Change", href: "/lettings-logs/#{duplicate_log.id}/tenant-code?first_remaining_duplicate_id=#{lettings_log.id}&original_log_id=#{lettings_log.id}&referrer=duplicate_logs") click_link("Change", href: "/lettings-logs/#{duplicate_log.id}/tenant-code?first_remaining_duplicate_id=#{lettings_log.id}&original_log_id=#{lettings_log.id}&referrer=duplicate_logs")
fill_in("lettings-log-tenancycode-field", with: "something else") fill_in("lettings-log-tenancycode-field", with: "something else")
click_button("Save and continue") click_button("Save changes")
expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}") expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Back to Log #{lettings_log.id}", href: "/lettings-logs/#{lettings_log.id}") expect(page).to have_link("Back to Log #{lettings_log.id}", href: "/lettings-logs/#{lettings_log.id}")
expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success")
expect(page).to have_content("Log #{duplicate_log.id} is no longer a duplicate and has been removed from the list") expect(page).to have_content("Log #{duplicate_log.id} is no longer a duplicate and has been removed from the list")
@ -521,8 +521,8 @@ RSpec.describe "Lettings Log Features" do
it "allows deduplicating logs by changing the answers on the original log" do it "allows deduplicating logs by changing the answers on the original log" do
click_link("Change", href: "/lettings-logs/#{lettings_log.id}/tenant-code?first_remaining_duplicate_id=#{duplicate_log.id}&original_log_id=#{lettings_log.id}&referrer=duplicate_logs") click_link("Change", href: "/lettings-logs/#{lettings_log.id}/tenant-code?first_remaining_duplicate_id=#{duplicate_log.id}&original_log_id=#{lettings_log.id}&referrer=duplicate_logs")
fill_in("lettings-log-tenancycode-field", with: "something else") fill_in("lettings-log-tenancycode-field", with: "something else")
click_button("Save and continue") click_button("Save changes")
expect(page).to have_current_path("/lettings-logs/#{duplicate_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}") expect(page).to have_current_path("/lettings-logs/#{duplicate_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Back to Log #{lettings_log.id}", href: "/lettings-logs/#{lettings_log.id}") expect(page).to have_link("Back to Log #{lettings_log.id}", href: "/lettings-logs/#{lettings_log.id}")
expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success")
expect(page).to have_content("Log #{lettings_log.id} is no longer a duplicate and has been removed from the list") expect(page).to have_content("Log #{lettings_log.id} is no longer a duplicate and has been removed from the list")

30
spec/features/organisation_spec.rb

@ -126,7 +126,8 @@ RSpec.describe "User Features" do
context "when viewing lettings logs for specific organisation" do context "when viewing lettings logs for specific organisation" do
let(:first_log) { organisation.lettings_logs.first } let(:first_log) { organisation.lettings_logs.first }
let!(:log_to_search) { FactoryBot.create(:lettings_log, created_by: user) } let!(:log_to_search) { FactoryBot.create(:lettings_log, created_by: user) }
let!(:other_logs) { FactoryBot.create_list(:lettings_log, 4, created_by: user) } let!(:other_general_needs_logs) { FactoryBot.create_list(:lettings_log, 4, created_by: user, needstype: 1) }
let!(:other_supported_housing_logs) { FactoryBot.create_list(:lettings_log, 4, created_by: user, needstype: 2) }
let(:number_of_lettings_logs) { LettingsLog.count } let(:number_of_lettings_logs) { LettingsLog.count }
before do before do
@ -148,7 +149,7 @@ RSpec.describe "User Features" do
context "when searching for specific logs" do context "when searching for specific logs" do
it "displays the logs belonging to the same organisation" do it "displays the logs belonging to the same organisation" do
expect(page).to have_content(log_to_search.id) expect(page).to have_content(log_to_search.id)
other_logs.each do |log| (other_general_needs_logs + other_supported_housing_logs).each do |log|
expect(page).to have_content(log.id) expect(page).to have_content(log.id)
end end
end end
@ -168,7 +169,7 @@ RSpec.describe "User Features" do
it "displays log matching the log ID" do it "displays log matching the log ID" do
expect(page).to have_link(log_to_search.id.to_s) expect(page).to have_link(log_to_search.id.to_s)
other_logs.each do |log| (other_general_needs_logs + other_supported_housing_logs).each do |log|
expect(page).not_to have_link(log.id.to_s) expect(page).not_to have_link(log.id.to_s)
end end
end end
@ -187,16 +188,31 @@ RSpec.describe "User Features" do
end end
end end
it "can filter lettings logs" do it "has correct page details" do
expect(page).to have_content("#{number_of_lettings_logs} total logs") expect(page).to have_content("#{number_of_lettings_logs} matching logs")
organisation.lettings_logs.map(&:id).each do |lettings_log_id| organisation.lettings_logs.map(&:id).each do |lettings_log_id|
expect(page).to have_link lettings_log_id.to_s, href: "/lettings-logs/#{lettings_log_id}" expect(page).to have_link lettings_log_id.to_s, href: "/lettings-logs/#{lettings_log_id}"
end end
end
it "can filter lettings logs by year" do
check("years-2021-field") check("years-2021-field")
click_button("Apply filters") click_button("Apply filters")
expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&years[]=2021&status[]=&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=") expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&years[]=2021&status[]=&needstypes[]=&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=")
expect(page).not_to have_link first_log.id.to_s, href: "/lettings-logs/#{first_log.id}" expect(page).not_to have_link first_log.id.to_s, href: "/lettings-logs/#{first_log.id}"
end end
it "can filter lettings logs by needstype" do
check("needstypes-1-field")
click_button("Apply filters")
expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&status[]=&needstypes[]=&needstypes[]=1&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=")
other_general_needs_logs.each do |general_needs_log|
expect(page).to have_link general_needs_log.id.to_s, href: "/lettings-logs/#{general_needs_log.id}"
end
other_supported_housing_logs.each do |supported_housing_log|
expect(page).not_to have_link supported_housing_log.id.to_s, href: "/lettings-logs/#{supported_housing_log.id}"
end
end
end end
context "when viewing sales logs for specific organisation" do context "when viewing sales logs for specific organisation" do
@ -221,7 +237,7 @@ RSpec.describe "User Features" do
end end
it "can filter sales logs" do it "can filter sales logs" do
expect(page).to have_content("#{number_of_sales_logs} total logs") expect(page).to have_content("#{number_of_sales_logs} matching logs")
organisation.sales_logs.map(&:id).each do |sales_log_id| organisation.sales_logs.map(&:id).each do |sales_log_id|
expect(page).to have_link sales_log_id.to_s, href: "/sales-logs/#{sales_log_id}" expect(page).to have_link sales_log_id.to_s, href: "/sales-logs/#{sales_log_id}"
end end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save