Browse Source

Merge branch 'refs/heads/main' into CLDC-3205-income-soft-validation-not-always-being-shown

# Conflicts:
#	spec/fixtures/files/lettings_log_csv_export_labels_24.csv
#	spec/fixtures/files/lettings_log_csv_export_labels_25.csv
pull/3302/head
samyou-softwire 3 weeks ago
parent
commit
ee7479f471
  1. 20
      .github/workflows/run_tests.yml
  2. 2
      .nvmrc
  3. 2
      .ruby-version
  4. 15
      Dockerfile
  5. 2
      Gemfile
  6. 2
      Gemfile.lock
  7. 38
      app/components/data_protection_confirmation_banner_component.rb
  8. 2
      app/frontend/controllers/numeric_question_controller.js
  9. 4
      app/helpers/collection_time_helper.rb
  10. 18
      app/helpers/filters_helper.rb
  11. 12
      app/models/form/lettings/pages/no_household_member_likely_to_be_pregnant_check.rb
  12. 4
      app/models/form/lettings/pages/property_local_authority.rb
  13. 2
      app/models/form/lettings/questions/builtype.rb
  14. 7
      app/models/form/lettings/questions/location_id.rb
  15. 2
      app/models/form/lettings/subsections/household_characteristics.rb
  16. 4
      app/models/form/sales/pages/property_local_authority.rb
  17. 11
      app/models/form/sales/questions/buyer_still_serving.rb
  18. 2
      app/models/form/sales/questions/property_building_type.rb
  19. 2
      app/models/form/sales/questions/purchase_price.rb
  20. 21
      app/models/lettings_log.rb
  21. 23
      app/models/user.rb
  22. 9
      app/models/validations/financial_validations.rb
  23. 2
      app/services/feature_toggle.rb
  24. 8
      app/services/filter_manager.rb
  25. 6
      app/views/form/headers/_person_2_known_page.erb
  26. 6
      app/views/form/headers/_person_3_known_page.erb
  27. 6
      app/views/form/headers/_person_4_known_page.erb
  28. 6
      app/views/form/headers/_person_5_known_page.erb
  29. 6
      app/views/form/headers/_person_6_known_page.erb
  30. 30
      app/views/users/_user_filters.html.erb
  31. 8
      aws-devcontainer/.devcontainer/Dockerfile
  32. 10
      config/locales/forms/2025/lettings/income_and_benefits.en.yml
  33. 8
      config/locales/forms/2025/sales/income_benefits_and_savings.en.yml
  34. 10
      config/locales/forms/2026/lettings/income_and_benefits.en.yml
  35. 8
      config/locales/forms/2026/sales/income_benefits_and_savings.en.yml
  36. 3
      config/locales/validations/lettings/financial.en.yml
  37. 4
      docs/Gemfile.lock
  38. 10
      docs/setup.md
  39. 34
      lib/tasks/delete_logs_in_collection_year_and_earlier.rake
  40. 129
      lib/tasks/log_la_fix.rake
  41. 24
      lib/tasks/remap_2025_hhregresstill_values.rake
  42. 11
      lib/tasks/round_value_for_2026_sales_logs.rake
  43. 10
      lib/tasks/update_logs_with_invalid_hb_benefits_2026.rake
  44. 2
      package.json
  45. 111
      spec/components/data_protection_confirmation_banner_component_spec.rb
  46. 9
      spec/features/form/progressive_total_field_spec.rb
  47. 20
      spec/features/user_spec.rb
  48. 2
      spec/fixtures/files/lettings_log_csv_export_labels_23.csv
  49. 6
      spec/fixtures/files/lettings_log_csv_export_labels_24.csv
  50. 2
      spec/fixtures/files/lettings_log_csv_export_labels_25.csv
  51. 2
      spec/fixtures/files/lettings_log_csv_export_non_support_labels_23.csv
  52. 2
      spec/fixtures/files/lettings_log_csv_export_non_support_labels_24.csv
  53. 2
      spec/fixtures/files/lettings_log_csv_export_non_support_labels_25.csv
  54. 2
      spec/fixtures/files/sales_logs_csv_export_labels_24.csv
  55. 2
      spec/fixtures/files/sales_logs_csv_export_labels_25.csv
  56. 2
      spec/fixtures/files/sales_logs_csv_export_labels_26.csv
  57. 2
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv
  58. 2
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_25.csv
  59. 2
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv
  60. 72
      spec/models/form/lettings/pages/property_local_authority_spec.rb
  61. 26
      spec/models/form/lettings/questions/location_id_spec.rb
  62. 49
      spec/models/form/sales/pages/property_building_type_spec.rb
  63. 61
      spec/models/form/sales/pages/property_local_authority_spec.rb
  64. 49
      spec/models/form/sales/pages/property_wheelchair_accessible_spec.rb
  65. 27
      spec/models/form/sales/questions/buyer_still_serving_spec.rb
  66. 2
      spec/models/form/sales/questions/property_building_type_spec.rb
  67. 39
      spec/models/lettings_log_derived_fields_spec.rb
  68. 104
      spec/models/validations/date_validations_spec.rb
  69. 101
      spec/models/validations/financial_validations_spec.rb
  70. 51
      spec/models/validations/sales/soft_validations_spec.rb
  71. 2
      spec/requests/check_errors_controller_spec.rb
  72. 6
      spec/requests/collection_resources_controller_spec.rb
  73. 197
      spec/services/csv/lettings_log_csv_service_spec.rb
  74. 47
      spec/services/filter_manager_spec.rb

20
.github/workflows/run_tests.yml

@ -38,7 +38,6 @@ jobs:
env: env:
RAILS_ENV: test RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost DB_HOST: localhost
DB_DATABASE: data_collector DB_DATABASE: data_collector
DB_USERNAME: postgres DB_USERNAME: postgres
@ -59,7 +58,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
cache: yarn cache: yarn
node-version: 20 node-version: 24
# This is temporary to fix flaky parallel tests due to `secret_key_base` being read before it's set # This is temporary to fix flaky parallel tests due to `secret_key_base` being read before it's set
- name: Create local secret - name: Create local secret
@ -102,7 +101,6 @@ jobs:
env: env:
RAILS_ENV: test RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost DB_HOST: localhost
DB_DATABASE: data_collector DB_DATABASE: data_collector
DB_USERNAME: postgres DB_USERNAME: postgres
@ -122,7 +120,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
cache: yarn cache: yarn
node-version: 20 node-version: 24
- name: Create database - name: Create database
run: | run: |
@ -160,7 +158,6 @@ jobs:
env: env:
RAILS_ENV: test RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost DB_HOST: localhost
DB_DATABASE: data_collector DB_DATABASE: data_collector
DB_USERNAME: postgres DB_USERNAME: postgres
@ -180,7 +177,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
cache: yarn cache: yarn
node-version: 20 node-version: 24
- name: Create database - name: Create database
run: | run: |
@ -218,7 +215,6 @@ jobs:
env: env:
RAILS_ENV: test RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost DB_HOST: localhost
DB_DATABASE: data_collector DB_DATABASE: data_collector
DB_USERNAME: postgres DB_USERNAME: postgres
@ -239,7 +235,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
cache: yarn cache: yarn
node-version: 20 node-version: 24
- name: Create local secret - name: Create local secret
run: | run: |
@ -281,7 +277,6 @@ jobs:
env: env:
RAILS_ENV: test RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost DB_HOST: localhost
DB_DATABASE: data_collector DB_DATABASE: data_collector
DB_USERNAME: postgres DB_USERNAME: postgres
@ -302,7 +297,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
cache: yarn cache: yarn
node-version: 20 node-version: 24
- name: Create local secret - name: Create local secret
run: | run: |
@ -344,7 +339,6 @@ jobs:
env: env:
RAILS_ENV: test RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost DB_HOST: localhost
DB_DATABASE: data_collector DB_DATABASE: data_collector
DB_USERNAME: postgres DB_USERNAME: postgres
@ -365,7 +359,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
cache: yarn cache: yarn
node-version: 20 node-version: 24
- name: Create database - name: Create database
run: | run: |
@ -396,7 +390,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
cache: yarn cache: yarn
node-version: 20 node-version: 24
- name: Install packages and symlink local dependencies - name: Install packages and symlink local dependencies
run: | run: |

2
.nvmrc

@ -1 +1 @@
20 24

2
.ruby-version

@ -1 +1 @@
3.4.4 3.4.9

15
Dockerfile

@ -1,7 +1,10 @@
FROM ruby:3.4.4-alpine3.20 as base FROM ruby:3.4.9-alpine3.23 as base
WORKDIR /app WORKDIR /app
# Upgrade base packages to pick up latest security patches
RUN apk upgrade --no-cache
# Add the timezone as it's not configured by default in Alpine # Add the timezone as it's not configured by default in Alpine
RUN apk add --update --no-cache tzdata && \ RUN apk add --update --no-cache tzdata && \
cp /usr/share/zoneinfo/Europe/London /etc/localtime && \ cp /usr/share/zoneinfo/Europe/London /etc/localtime && \
@ -10,7 +13,7 @@ RUN apk add --update --no-cache tzdata && \
# build-base: compilation tools for bundle # build-base: compilation tools for bundle
# yarn: node package manager # yarn: node package manager
# postgresql-dev: postgres driver and libraries # postgresql-dev: postgres driver and libraries
RUN apk add --no-cache build-base=0.5-r3 busybox=1.36.1-r29 nodejs=20.15.1-r0 yarn=1.22.22-r0 bash=5.2.26-r0 libpq-dev yaml-dev linux-headers RUN apk add --no-cache build-base busybox nodejs yarn bash libpq-dev yaml-dev linux-headers
# Bundler version should be the same version as what the Gemfile.lock was bundled with # Bundler version should be the same version as what the Gemfile.lock was bundled with
RUN gem install bundler:2.6.4 --no-document RUN gem install bundler:2.6.4 --no-document
@ -40,14 +43,14 @@ RUN bundle config set without ""
RUN bundle install --jobs=4 --no-binstubs --no-cache RUN bundle install --jobs=4 --no-binstubs --no-cache
# Install gecko driver for Capybara tests # Install gecko driver for Capybara tests
RUN apk add firefox RUN apk add firefox=145.0-r0
RUN wget https://github.com/mozilla/geckodriver/releases/download/v0.31.0/geckodriver-v0.31.0-linux64.tar.gz \ RUN wget https://github.com/mozilla/geckodriver/releases/download/v0.31.0/geckodriver-v0.31.0-linux64.tar.gz \
&& tar -xvzf geckodriver-v0.31.0-linux64.tar.gz \ && tar -xvzf geckodriver-v0.31.0-linux64.tar.gz \
&& rm geckodriver-v0.31.0-linux64.tar.gz \ && rm geckodriver-v0.31.0-linux64.tar.gz \
&& chmod +x geckodriver \ && chmod +x geckodriver \
&& mv geckodriver /usr/local/bin/ && mv geckodriver /usr/local/bin/
CMD bundle exec rake parallel:setup && bundle exec rake parallel:spec CMD ["sh", "-c", "bundle exec rake parallel:setup && bundle exec rake parallel:spec"]
FROM base as development FROM base as development
@ -61,7 +64,7 @@ RUN bundle install --jobs=4 --no-binstubs --no-cache
USER nonroot USER nonroot
CMD bundle exec rails s -e ${RAILS_ENV} -p ${PORT} --binding=0.0.0.0 CMD ["sh", "-c", "bundle exec rails s -e ${RAILS_ENV} -p ${PORT} --binding=0.0.0.0"]
FROM base as production FROM base as production
@ -75,4 +78,4 @@ RUN chown -R nonroot performance_test
USER nonroot USER nonroot
CMD bundle exec rails s -e ${RAILS_ENV} -p ${PORT} --binding=0.0.0.0 CMD ["sh", "-c", "bundle exec rails s -e ${RAILS_ENV} -p ${PORT} --binding=0.0.0.0"]

2
Gemfile

@ -3,7 +3,7 @@
source "https://rubygems.org" source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" } git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.4.4" ruby "3.4.9"
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem "rails", "~> 7.2.2" gem "rails", "~> 7.2.2"

2
Gemfile.lock

@ -648,7 +648,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.4.4p0 ruby 3.4.9p82
BUNDLED WITH BUNDLED WITH
2.6.4 2.6.4

38
app/components/data_protection_confirmation_banner_component.rb

@ -12,16 +12,16 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
def display_banner? def display_banner?
return false if user.support? && organisation.blank? return false if user.support? && organisation.blank?
return true if org_without_dpo? return true if show_no_dpo_message?
return false if !org_or_user_org.holds_own_stock? && org_or_user_org.stock_owners.empty? && org_or_user_org.absorbed_organisations.empty? return false if !org_or_user_org.holds_own_stock? && org_or_user_org.stock_owners.empty? && org_or_user_org.absorbed_organisations.empty?
!org_or_user_org.data_protection_confirmed? || !org_or_user_org.organisation_or_stock_owner_signed_dsa_and_holds_own_stock? !dsa_signed? || !org_or_user_org.organisation_or_stock_owner_signed_dsa_and_holds_own_stock?
end end
def header_text def header_text
if org_without_dpo? if show_no_dpo_message?
"To create logs your organisation must state a data protection officer. They must sign the Data Sharing Agreement." "To create logs your organisation must state a data protection officer. They must sign the Data Sharing Agreement."
elsif !org_or_user_org.holds_own_stock? && org_or_user_org.data_protection_confirmed? elsif show_no_stock_owner_message?
"Your organisation does not own stock. To create logs your stock owner(s) must accept the Data Sharing Agreement on CORE." "Your organisation does not own stock. To create logs your stock owner(s) must accept the Data Sharing Agreement on CORE."
elsif user.is_dpo? elsif user.is_dpo?
"Your organisation must accept the Data Sharing Agreement before you can create any logs." "Your organisation must accept the Data Sharing Agreement before you can create any logs."
@ -31,7 +31,7 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
end end
def banner_text def banner_text
if org_without_dpo? || user.is_dpo? || !org_or_user_org.holds_own_stock? if show_no_dpo_message? || user.is_dpo? || !org_or_user_org.holds_own_stock?
govuk_link_to( govuk_link_to(
link_text, link_text,
link_href, link_href,
@ -51,9 +51,9 @@ private
end end
def link_text def link_text
if dpo_required? if show_no_dpo_message?
"Contact helpdesk to assign a data protection officer" "Contact helpdesk to assign a data protection officer"
elsif !org_or_user_org.holds_own_stock? && org_or_user_org.data_protection_confirmed? elsif show_no_stock_owner_message?
"View or add stock owners" "View or add stock owners"
else else
"Read the Data Sharing Agreement" "Read the Data Sharing Agreement"
@ -61,24 +61,32 @@ private
end end
def link_href def link_href
if dpo_required? if show_no_dpo_message?
GlobalConstants::HELPDESK_URL GlobalConstants::HELPDESK_URL
elsif !org_or_user_org.holds_own_stock? && org_or_user_org.data_protection_confirmed? elsif show_no_stock_owner_message?
stock_owners_organisation_path(org_or_user_org) stock_owners_organisation_path(org_or_user_org)
else else
data_sharing_agreement_organisation_path(org_or_user_org) data_sharing_agreement_organisation_path(org_or_user_org)
end end
end end
def dpo_required? def show_no_dpo_message?
org_or_user_org.data_protection_officers.empty? # it is fine if an org has a DSA and the DPO has moved on
# CORE staff do this sometimes as a single DPO covers multiple 'orgs' that exist as branches of the same real world org
# so, they move the DPO to all the mini orgs and have them sign each DSA
# so the DSA being signed can silence this warning
org_or_user_org.data_protection_officers.empty? && !dsa_signed?
end end
def org_or_user_org def dsa_signed?
organisation.presence || user.organisation org_or_user_org.data_protection_confirmed?
end
def show_no_stock_owner_message?
!org_or_user_org.holds_own_stock? && dsa_signed?
end end
def org_without_dpo? def org_or_user_org
org_or_user_org.data_protection_officers.empty? organisation.presence || user.organisation
end end
end end

2
app/frontend/controllers/numeric_question_controller.js

@ -11,7 +11,7 @@ export default class extends Controller {
calculateFields () { calculateFields () {
const affectedField = this.element.dataset.target const affectedField = this.element.dataset.target
const fieldsToAdd = JSON.parse(this.element.dataset.calculated).map(x => `lettings-log-${x.replaceAll('_', '-')}-field`) const fieldsToAdd = JSON.parse(this.element.dataset.calculated).map(x => `lettings-log-${x.replaceAll('_', '-')}-field`)
const valuesToAdd = fieldsToAdd.map(x => getFieldValue(x)).filter(x => x) const valuesToAdd = fieldsToAdd.map(x => getFieldValue(x)).filter(x => x && !isNaN(parseFloat(x)))
const newValue = valuesToAdd.map(x => parseFloat(x)).reduce((a, b) => a + b, 0).toFixed(2) const newValue = valuesToAdd.map(x => parseFloat(x)).reduce((a, b) => a + b, 0).toFixed(2)
const elementToUpdate = document.getElementById(affectedField) const elementToUpdate = document.getElementById(affectedField)
elementToUpdate.value = newValue elementToUpdate.value = newValue

4
app/helpers/collection_time_helper.rb

@ -29,6 +29,10 @@ module CollectionTimeHelper
Time.zone.local(current_collection_start_year, 4, 1) Time.zone.local(current_collection_start_year, 4, 1)
end end
def current_collection_after_crossover_start_date
Form::DEADLINES[current_collection_start_year][:edit_end_date] + 1.day
end
def collection_end_date(date) def collection_end_date(date)
Time.zone.local(collection_start_year_for_date(date) + 1, 3, 31).end_of_day Time.zone.local(collection_start_year_for_date(date) + 1, 3, 31).end_of_day
end end

18
app/helpers/filters_helper.rb

@ -52,6 +52,22 @@ module FiltersHelper
}.freeze }.freeze
end end
def user_role_type_filters(include_support: false)
roles = {
"data_provider" => "Data provider",
"data_coordinator" => "Data coordinator",
}
roles["support"] = "Support" if include_support
roles.freeze
end
def user_additional_responsibilities_filters
{
"data_protection_officer" => "Data protection officer",
"key_contact" => "Key contact",
}.freeze
end
def scheme_status_filters def scheme_status_filters
{ {
"incomplete" => "Incomplete", "incomplete" => "Incomplete",
@ -306,7 +322,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[years status needstypes bulk_upload_id].include?(category) if %w[years status needstypes bulk_upload_id role additional_responsibilities].include?(category)
category_filters.count(&:present?) category_filters.count(&:present?)
elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search uploading_organisation].include?(category) elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search uploading_organisation].include?(category)
1 1

12
app/models/form/lettings/pages/no_household_member_likely_to_be_pregnant_check.rb

@ -2,7 +2,8 @@ class Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck < ::Form::
def initialize(id, hsh, subsection, person_index: 0) def initialize(id, hsh, subsection, person_index: 0)
super(id, hsh, subsection) super(id, hsh, subsection)
@copy_key = "lettings.soft_validations.pregnancy_value_check.no_household_member_likely_to_be_pregnant_check" @copy_key = "lettings.soft_validations.pregnancy_value_check.no_household_member_likely_to_be_pregnant_check"
@depends_on = [{ "no_household_member_likely_to_be_pregnant?" => true }] @person_index = person_index
@depends_on = depends_on
@title_text = { @title_text = {
"translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text", "translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text",
"arguments" => [], "arguments" => [],
@ -11,7 +12,14 @@ class Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck < ::Form::
"translation" => "forms.#{form.start_date.year}.#{@copy_key}.informative_text", "translation" => "forms.#{form.start_date.year}.#{@copy_key}.informative_text",
"arguments" => [], "arguments" => [],
} }
@person_index = person_index end
def depends_on
if @person_index >= 2
[{ "no_household_member_likely_to_be_pregnant?" => true, "details_known_#{@person_index}" => 0 }]
else
[{ "no_household_member_likely_to_be_pregnant?" => true }]
end
end end
def questions def questions

4
app/models/form/lettings/pages/property_local_authority.rb

@ -3,8 +3,8 @@ class Form::Lettings::Pages::PropertyLocalAuthority < ::Form::Page
super super
@id = "property_local_authority" @id = "property_local_authority"
@depends_on = [ @depends_on = [
{ "is_la_inferred" => false, "is_general_needs?" => true, "form.start_year_2024_or_later?" => false }, { "is_la_inferred" => false, "is_general_needs?" => true, "form.start_year_2025_or_later?" => false, "address_search_given?" => true },
{ "is_la_inferred" => false, "is_general_needs?" => true, "form.start_year_2024_or_later?" => true, "address_search_given?" => true }, { "is_la_inferred" => false, "is_general_needs?" => true, "form.start_year_2025_or_later?" => true },
] ]
end end

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

@ -9,7 +9,7 @@ class Form::Lettings::Questions::Builtype < ::Form::Question
ANSWER_OPTIONS = { ANSWER_OPTIONS = {
"2" => { "value" => "Converted from previous residential or non-residential property" }, "2" => { "value" => "Converted from previous residential or non-residential property" },
"1" => { "value" => "Purpose built" }, "1" => { "value" => "Purpose-built" },
}.freeze }.freeze
QUESTION_NUMBER_FROM_YEAR = { 2023 => 20, 2024 => 20, 2025 => 20 }.freeze QUESTION_NUMBER_FROM_YEAR = { 2023 => 20, 2024 => 20, 2025 => 20 }.freeze

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

@ -30,11 +30,8 @@ class Form::Lettings::Questions::LocationId < ::Form::Question
scheme_location_ids = lettings_log.scheme.locations.visible.confirmed.pluck(:id) scheme_location_ids = lettings_log.scheme.locations.visible.confirmed.pluck(:id)
answer_options.select { |k, _v| scheme_location_ids.include?(k.to_i) } answer_options.select { |k, _v| scheme_location_ids.include?(k.to_i) }
.sort_by { |_, v| .sort_by { |_, v| v["value"] }
name = v["hint"].match(/[a-zA-Z].*/).to_s .to_h
number = v["hint"].match(/\d+/).to_s.to_i
[name, number]
}.to_h
end end
def hidden_in_check_answers?(lettings_log, _current_user = nil) def hidden_in_check_answers?(lettings_log, _current_user = nil)

2
app/models/form/lettings/subsections/household_characteristics.rb

@ -17,7 +17,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::LeadTenantAge.new(nil, nil, self), Form::Lettings::Pages::LeadTenantAge.new(nil, nil, self),
(Form::Lettings::Pages::NoFemalesPregnantHouseholdLeadAgeValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?), (Form::Lettings::Pages::NoFemalesPregnantHouseholdLeadAgeValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdLeadAgeValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?), (Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdLeadAgeValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_lead_age_check", nil, self) if form.start_year_2026_or_later?), (Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_lead_age_check", nil, self, person_index: 1) if form.start_year_2026_or_later?),
Form::Lettings::Pages::LeadTenantUnderRetirementValueCheck.new("age_lead_tenant_under_retirement_value_check", nil, self), Form::Lettings::Pages::LeadTenantUnderRetirementValueCheck.new("age_lead_tenant_under_retirement_value_check", nil, self),
Form::Lettings::Pages::LeadTenantOverRetirementValueCheck.new("age_lead_tenant_over_retirement_value_check", nil, self), Form::Lettings::Pages::LeadTenantOverRetirementValueCheck.new("age_lead_tenant_over_retirement_value_check", nil, self),
(Form::Lettings::Pages::LeadTenantSexRegisteredAtBirth.new(nil, nil, self) if form.start_year_2026_or_later?), (Form::Lettings::Pages::LeadTenantSexRegisteredAtBirth.new(nil, nil, self) if form.start_year_2026_or_later?),

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

@ -3,8 +3,8 @@ class Form::Sales::Pages::PropertyLocalAuthority < ::Form::Page
super super
@id = "property_local_authority" @id = "property_local_authority"
@depends_on = [ @depends_on = [
{ "is_la_inferred" => false, "form.start_year_2024_or_later?" => false }, { "is_la_inferred" => false, "form.start_year_2025_or_later?" => false, "address_search_given?" => true },
{ "is_la_inferred" => false, "form.start_year_2024_or_later?" => true, "address_search_given?" => true }, { "is_la_inferred" => false, "form.start_year_2025_or_later?" => true },
] ]
end end

11
app/models/form/sales/questions/buyer_still_serving.rb

@ -19,13 +19,18 @@ class Form::Sales::Questions::BuyerStillServing < ::Form::Question
else else
{ {
"4" => { "value" => "Yes" }, "4" => { "value" => "Yes" },
"5" => { "value" => "No" }, "5" => { "value" => "No - they left up to and including 2 years ago" },
"6" => { "value" => "Buyer prefers not to say" }, "6" => { "value" => "No - they left more than 2 years ago" },
"divider" => { "value" => true }, "divider" => { "value" => true },
"7" => { "value" => "Don’t know" }, "9" => { "value" => "Don’t know" },
"10" => { "value" => "No" },
}.freeze }.freeze
end end
end end
def displayed_answer_options(_log, _user = nil)
answer_options.reject { |key, _v| key == "10" }
end
QUESTION_NUMBER_FROM_YEAR = { 2023 => 63, 2024 => 65, 2025 => 62, 2026 => 70 }.freeze QUESTION_NUMBER_FROM_YEAR = { 2023 => 63, 2024 => 65, 2025 => 62, 2026 => 70 }.freeze
end end

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

@ -9,7 +9,7 @@ class Form::Sales::Questions::PropertyBuildingType < ::Form::Question
end end
ANSWER_OPTIONS = { ANSWER_OPTIONS = {
"1" => { "value" => "Purpose built" }, "1" => { "value" => "Purpose-built" },
"2" => { "value" => "Converted from previous residential or non-residential property" }, "2" => { "value" => "Converted from previous residential or non-residential property" },
}.freeze }.freeze

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

@ -4,7 +4,7 @@ class Form::Sales::Questions::PurchasePrice < ::Form::Question
@id = "value" @id = "value"
@type = "numeric" @type = "numeric"
@min = form.start_year_2026_or_later? ? 15_000 : 0 @min = form.start_year_2026_or_later? ? 15_000 : 0
@step = 0.01 @step = form.start_year_2026_or_later? ? 1 : 0.01 # 0.01 was a mistake that was fixed in 2026
@width = 5 @width = 5
@prefix = "£" @prefix = "£"
@ownership_sch = ownershipsch @ownership_sch = ownershipsch

21
app/models/lettings_log.rb

@ -542,7 +542,7 @@ class LettingsLog < Log
reason == 1 reason == 1
end end
def receives_housing_benefit_only? def receives_housing_benefit?
# 1: Housing benefit # 1: Housing benefit
hb == 1 hb == 1
end end
@ -551,13 +551,7 @@ class LettingsLog < Log
hb == 3 hb == 3
end end
# Option 8 has been removed starting from 22/23 def receives_universal_credit
def receives_housing_benefit_and_universal_credit?
# 8: Housing benefit and Universal Credit (without housing element)
hb == 8
end
def receives_uc_with_housing_element_excl_housing_benefit?
# 6: Universal Credit with housing element (excluding housing benefit) # 6: Universal Credit with housing element (excluding housing benefit)
hb == 6 hb == 6
end end
@ -572,12 +566,11 @@ class LettingsLog < Log
end end
def receives_housing_related_benefits? def receives_housing_related_benefits?
if collection_start_year <= 2021 receives_housing_benefit? || receives_universal_credit
receives_housing_benefit_only? || receives_uc_with_housing_element_excl_housing_benefit? || end
receives_housing_benefit_and_universal_credit?
else def no_household_income_comes_from_benefits?
receives_housing_benefit_only? || receives_uc_with_housing_element_excl_housing_benefit? benefits == 3
end
end end
def local_housing_referral? def local_housing_referral?

23
app/models/user.rb

@ -81,6 +81,29 @@ class User < ApplicationRecord
filtered_records filtered_records
} }
scope :filter_by_role, ->(role, _user = nil) { where(role:) }
scope :filter_by_additional_responsibilities, lambda { |additional_responsibilities, _user|
filtered_records = all
scopes = []
additional_responsibilities.each do |responsibility|
case responsibility
when "key_contact"
scopes << is_key_contact
when "data_protection_officer"
scopes << is_data_protection_officer
end
end
if scopes.any?
filtered_records = filtered_records.merge(scopes.reduce(&:or))
end
filtered_records
}
scope :is_key_contact, -> { where(is_key_contact: true) }
scope :is_data_protection_officer, -> { where(is_dpo: true) }
scope :not_signed_in, -> { where(last_sign_in_at: nil, active: true) } scope :not_signed_in, -> { where(last_sign_in_at: nil, active: true) }
scope :deactivated, -> { where(active: false) } scope :deactivated, -> { where(active: false) }
scope :activated, -> { where(active: true) } scope :activated, -> { where(active: true) }

9
app/models/validations/financial_validations.rb

@ -175,6 +175,15 @@ module Validations::FinancialValidations
end end
end end
def validate_housing_benefits_matches_income_proportion(record)
return unless record.hb && record.benefits && record.form.start_year_2026_or_later?
if (record.receives_universal_credit || record.receives_housing_benefit?) && record.no_household_income_comes_from_benefits?
record.errors.add :hb, I18n.t("validations.lettings.financial.hb.housing_benefits_not_match_income_source")
record.errors.add :benefits, I18n.t("validations.lettings.financial.benefits.housing_benefits_not_match_income_source")
end
end
private private
def validate_charges(record) def validate_charges(record)

2
app/services/feature_toggle.rb

@ -28,7 +28,7 @@ class FeatureToggle
end end
def self.create_test_logs_enabled? def self.create_test_logs_enabled?
Rails.env.development? || Rails.env.review? || Rails.env.staging? Rails.env.development? || Rails.env.review?
end end
def self.sales_export_enabled? def self.sales_export_enabled?

8
app/services/filter_manager.rb

@ -130,6 +130,14 @@ class FilterManager
new_filters["status"] = params["status"] new_filters["status"] = params["status"]
end end
if filter_type.include?("users") && params["role"].present?
new_filters["role"] = params["role"]
end
if filter_type.include?("users") && params["additional_responsibilities"].present?
new_filters["additional_responsibilities"] = params["additional_responsibilities"]
end
if filter_type.include?("schemes") if filter_type.include?("schemes")
current_user.scheme_filters(specific_org:).each do |filter| current_user.scheme_filters(specific_org:).each do |filter|
new_filters[filter] = params[filter] if params[filter].present? new_filters[filter] = params[filter] if params[filter].present?

6
app/views/form/headers/_person_2_known_page.erb

@ -1 +1,5 @@
You have given us the details for 0 of the <%= log.hholdcount %> other people in the household <% if log.form.start_year_2026_or_later? %>
You have given us the details for 1 of the <%= log.hholdcount %> people in the household
<% else %>
You have given us the details for 0 of the <%= log.hholdcount %> other people in the household
<% end %>

6
app/views/form/headers/_person_3_known_page.erb

@ -1 +1,5 @@
You have given us the details for <%= log.joint_purchase? ? 0 : 1 %> of the <%= log.hholdcount %> other people in the household <% if log.form.start_year_2026_or_later? %>
You have given us the details for 2 of the <%= log.hholdcount %> people in the household
<% else %>
You have given us the details for <%= log.joint_purchase? ? 0 : 1 %> of the <%= log.hholdcount %> other people in the household
<% end %>

6
app/views/form/headers/_person_4_known_page.erb

@ -1 +1,5 @@
You have given us the details for <%= log.joint_purchase? ? 1 : 2 %> of the <%= log.hholdcount %> other people in the household <% if log.form.start_year_2026_or_later? %>
You have given us the details for 3 of the <%= log.hholdcount %> people in the household
<% else %>
You have given us the details for <%= log.joint_purchase? ? 1 : 2 %> of the <%= log.hholdcount %> other people in the household
<% end %>

6
app/views/form/headers/_person_5_known_page.erb

@ -1 +1,5 @@
You have given us the details for <%= log.joint_purchase? ? 2 : 3 %> of the <%= log.hholdcount %> other people in the household <% if log.form.start_year_2026_or_later? %>
You have given us the details for 4 of the <%= log.hholdcount %> people in the household
<% else %>
You have given us the details for <%= log.joint_purchase? ? 2 : 3 %> of the <%= log.hholdcount %> other people in the household
<% end %>

6
app/views/form/headers/_person_6_known_page.erb

@ -1 +1,5 @@
You have given us the details for <%= log.joint_purchase? ? 3 : 4 %> of the <%= log.hholdcount %> other people in the household <% if log.form.start_year_2026_or_later? %>
You have given us the details for 5 of the <%= log.hholdcount %> people in the household
<% else %>
You have given us the details for <%= log.joint_purchase? ? 3 : 4 %> of the <%= log.hholdcount %> other people in the household
<% end %>

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

@ -17,12 +17,30 @@
<%= render partial: "filters/checkbox_filter", <%= render partial: "filters/checkbox_filter",
locals: { locals: {
f:, f:,
options: user_status_filters, options: user_status_filters,
label: "Status", label: "Status",
category: "status", category: "status",
size: "s", size: "s",
} %> } %>
<%= render partial: "filters/checkbox_filter",
locals: {
f:,
options: user_role_type_filters(include_support: current_user.support?),
label: "Role type",
category: "role",
size: "s",
} %>
<%= render partial: "filters/checkbox_filter",
locals: {
f:,
options: user_additional_responsibilities_filters,
label: "Additional responsibilities",
category: "additional_responsibilities",
size: "s",
} %>
<% if request.params["search"].present? %> <% if request.params["search"].present? %>
<%= f.hidden_field :search, value: request.params["search"] %> <%= f.hidden_field :search, value: request.params["search"] %>

8
aws-devcontainer/.devcontainer/Dockerfile

@ -1,7 +1,13 @@
FROM homebrew/brew FROM homebrew/brew
RUN brew install aws-vault && brew install awscli RUN brew install aws-vault && brew install awscli
RUN curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb" && sudo dpkg -i session-manager-plugin.deb RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
ARCH="ubuntu_arm64"; \
else \
ARCH="ubuntu_64bit"; \
fi && \
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/${ARCH}/session-manager-plugin.deb" -o "session-manager-plugin.deb" && \
sudo dpkg -i session-manager-plugin.deb
ENV AWS_VAULT_BACKEND=file ENV AWS_VAULT_BACKEND=file
ENV AWS_VAULT_FILE_DIR=./vault ENV AWS_VAULT_FILE_DIR=./vault

10
config/locales/forms/2025/lettings/income_and_benefits.en.yml

@ -25,10 +25,10 @@ en:
hb: hb:
page_header: "" page_header: ""
check_answer_label: "Housing related benefits received" check_answer_label: "Housing-related benefits received"
check_answer_prompt: "Tell us if household receives housing related benefits" check_answer_prompt: "Tell us if household receives housing-related benefits"
hint_text: "This is about when the tenant is in their new let. If they are unsure about the situation for their new let and their financial and working situation hasn’t changed significantly, answer based on what housing related benefits they currently receive." hint_text: "This is about when the tenant is in their new let. If they are unsure about the situation for their new let and their financial and working situation hasn’t changed significantly, answer based on what housing-related benefits they currently receive."
question_text: "Is the household likely to be receiving any of these housing related benefits?" question_text: "Is the household likely to be receiving any of these housing-related benefits?"
benefits: benefits:
page_header: "" page_header: ""
@ -112,7 +112,7 @@ en:
check_answer_label: "Any outstanding amount for basic rent and charges" check_answer_label: "Any outstanding amount for basic rent and charges"
check_answer_prompt: "Tell us if any outstanding amount for basic rent and charges" check_answer_prompt: "Tell us if any outstanding amount for basic rent and charges"
hint_text: "Also known as the ‘outstanding amount’." hint_text: "Also known as the ‘outstanding amount’."
question_text: "After the household has received any housing related benefits, will they still need to pay for rent and charges?" question_text: "After the household has received any housing-related benefits, will they still need to pay for rent and charges?"
outstanding_amount: outstanding_amount:
page_header: "" page_header: ""

8
config/locales/forms/2025/sales/income_benefits_and_savings.en.yml

@ -46,16 +46,16 @@ en:
housing_benefits: housing_benefits:
joint_purchase: joint_purchase:
page_header: "" page_header: ""
check_answer_label: "Housing related benefits buyers received before buying this property" check_answer_label: "Housing-related benefits buyers received before buying this property"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Were the buyers receiving any of these housing related benefits immediately before buying this property?" question_text: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?"
not_joint_purchase: not_joint_purchase:
page_header: "" page_header: ""
check_answer_label: "Housing related benefits buyer received before buying this property" check_answer_label: "Housing-related benefits buyer received before buying this property"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Was the buyer receiving any of these housing related benefits immediately before buying this property?" question_text: "Was the buyer receiving any of these housing-related benefits immediately before buying this property?"
savings: savings:
joint_purchase: joint_purchase:

10
config/locales/forms/2026/lettings/income_and_benefits.en.yml

@ -25,10 +25,10 @@ en:
hb: hb:
page_header: "" page_header: ""
check_answer_label: "Housing related benefits received" check_answer_label: "Housing-related benefits received"
check_answer_prompt: "Tell us if household receives housing related benefits" check_answer_prompt: "Tell us if household receives housing-related benefits"
hint_text: "This is about when the tenant is in their new let. If they are unsure about the situation for their new let and their financial and working situation hasn’t changed significantly, answer based on what housing related benefits they currently receive." hint_text: "This is about when the tenant is in their new let. If they are unsure about the situation for their new let and their financial and working situation hasn’t changed significantly, answer based on what housing-related benefits they currently receive."
question_text: "Is the household likely to be receiving any of these housing related benefits?" question_text: "Is the household likely to be receiving any of these housing-related benefits?"
benefits: benefits:
page_header: "" page_header: ""
@ -112,7 +112,7 @@ en:
check_answer_label: "Any outstanding amount for basic rent and charges" check_answer_label: "Any outstanding amount for basic rent and charges"
check_answer_prompt: "Tell us if any outstanding amount for basic rent and charges" check_answer_prompt: "Tell us if any outstanding amount for basic rent and charges"
hint_text: "Also known as the ‘outstanding amount’." hint_text: "Also known as the ‘outstanding amount’."
question_text: "After the household has received any housing related benefits, will they still need to pay for rent and charges?" question_text: "After the household has received any housing-related benefits, will they still need to pay for rent and charges?"
outstanding_amount: outstanding_amount:
page_header: "" page_header: ""

8
config/locales/forms/2026/sales/income_benefits_and_savings.en.yml

@ -46,16 +46,16 @@ en:
housing_benefits: housing_benefits:
joint_purchase: joint_purchase:
page_header: "" page_header: ""
check_answer_label: "Housing related benefits buyers received before buying this property" check_answer_label: "Housing-related benefits buyers received before buying this property"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Were the buyers receiving any of these housing related benefits immediately before buying this property?" question_text: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?"
not_joint_purchase: not_joint_purchase:
page_header: "" page_header: ""
check_answer_label: "Housing related benefits buyer received before buying this property" check_answer_label: "Housing-related benefits buyer received before buying this property"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Was the buyer receiving any of these housing related benefits immediately before buying this property?" question_text: "Was the buyer receiving any of these housing-related benefits immediately before buying this property?"
savings: savings:
joint_purchase: joint_purchase:

3
config/locales/validations/lettings/financial.en.yml

@ -12,6 +12,7 @@ en:
outstanding_amount_not_expected: "Answer must be ‘yes’ as you have answered the outstanding amount question." outstanding_amount_not_expected: "Answer must be ‘yes’ as you have answered the outstanding amount question."
benefits: benefits:
part_or_full_time: "Answer cannot be ‘all’ for income from Universal Credit, state pensions or benefits if the tenant or their partner works part-time or full-time." part_or_full_time: "Answer cannot be ‘all’ for income from Universal Credit, state pensions or benefits if the tenant or their partner works part-time or full-time."
housing_benefits_not_match_income_source: "You answered that none of the household’s income is from Universal Credit, state pensions or benefits, but also that the tenant is likely to be receiving Universal Credit or housing benefit."
earnings: earnings:
over_hard_max: "The household’s income cannot be greater than %{hard_max} per week given the household’s working situation." over_hard_max: "The household’s income cannot be greater than %{hard_max} per week given the household’s working situation."
under_hard_min: "The household’s income cannot be less than %{hard_min} per week given the household’s working situation." under_hard_min: "The household’s income cannot be less than %{hard_min} per week given the household’s working situation."
@ -87,3 +88,5 @@ en:
needstype: needstype:
rent_below_hard_min: "Rent is below the absolute minimum expected for a property of this type based on this lettings type." rent_below_hard_min: "Rent is below the absolute minimum expected for a property of this type based on this lettings type."
rent_above_hard_max: "Rent is higher than the absolute maximum expected for a property of this type based on this lettings type." rent_above_hard_max: "Rent is higher than the absolute maximum expected for a property of this type based on this lettings type."
hb:
housing_benefits_not_match_income_source: "You answered that none of the household’s income is from Universal Credit, state pensions or benefits, but also that the tenant is likely to be receiving Universal Credit or housing benefit."

4
docs/Gemfile.lock

@ -13,8 +13,8 @@ GEM
minitest (>= 5.1, < 6) minitest (>= 5.1, < 6)
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.1) addressable (2.9.0)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 8.0)
base64 (0.3.0) base64 (0.3.0)
benchmark (0.5.0) benchmark (0.5.0)
bigdecimal (4.0.1) bigdecimal (4.0.1)

10
docs/setup.md

@ -70,21 +70,19 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage NodeJS version
4. Install Ruby and Bundler 4. Install Ruby and Bundler
```bash ```bash
rbenv install 3.4.4 rbenv install 3.4.9
rbenv global 3.4.4 rbenv global 3.4.9
source ~/.bashrc source ~/.bashrc
gem install bundler gem install bundler
``` ```
5. Install JavaScript dependencies 5. Install JavaScript dependencies
Note that we currently use node v16, which is no longer the latest LTS version so you will need to specify the version number when installing
macOS (using nvm): macOS (using nvm):
```bash ```bash
nvm install 20 nvm install 24
nvm use 20 nvm use 24
brew install yarn brew install yarn
``` ```

34
lib/tasks/delete_logs_in_collection_year_and_earlier.rake

@ -0,0 +1,34 @@
desc "Deletes all logs in a given collection year and earlier. Note that this operation is PERMANENT and this will bypass callbacks/paper trail. Use only as instructed in a yearly cleanup task."
task :delete_logs_in_collection_year_and_earlier, %i[year] => :environment do |_task, args|
year = args[:year].to_i
if year < 2020
raise ArgumentError, "Year must be above 2020. Make sure you've written out the entire year"
end
if year > Time.zone.now.year - 3
raise ArgumentError, "Year cannot be the last 3 years, as these may contain visible logs"
end
puts "Deleting Logs before #{year}"
puts "Deleting Sales Logs in batches of 10000"
logs = SalesLog.filter_by_year_or_earlier(year)
logs.in_batches(of: 10_000).each_with_index do |logs, i|
puts "Deleting batch #{i + 1}"
logs.delete_all
end
puts "Done deleting Sales Logs"
puts "Deleting Lettings Logs in batches of 10000"
logs = LettingsLog.filter_by_year_or_earlier(year)
logs.in_batches(of: 10_000).each_with_index do |logs, i|
puts "Deleting batch #{i + 1}"
logs.delete_all
end
puts "Done deleting Lettings Logs"
puts "Done deleting Logs before #{year}"
end

129
lib/tasks/log_la_fix.rake

@ -0,0 +1,129 @@
namespace :log_la_fix do
desc "For all logs missing an LA that could have one, call the postcode changed method to request a new one from postcodes API. For logs where an LA still cannot be found, this will set them back to in progress."
task :search_for_la_on_logs_with_nil_la, [:year] => :environment do |_task, args|
include CollectionTimeHelper
year = args[:year]&.to_i || current_collection_start_year
lettings_logs = LettingsLog.filter_by_year(year).where(la: nil, needstype: 1, status: "completed")
sales_logs = SalesLog.filter_by_year(year).where(la: nil, status: "completed")
lettings_logs_count = lettings_logs.count
sales_logs_count = sales_logs.count
puts "Checking LA on #{lettings_logs_count} lettings logs in #{year}"
i = 0
lettings_logs.find_each do |log|
next unless log.valid?
log.process_postcode_changes!
unless log.save
puts "Failed to save lettings log #{log.id}"
puts "Errors: #{log.errors.full_messages}"
end
if log.la.nil? && log.status == "in_progress"
puts "#lettings##{log.id},\"#{log.tenancycode}\",\"#{log.propcode}\",#{log.owning_organisation_id},\"#{log.owning_organisation&.name}\",#{log.managing_organisation_id},\"#{log.managing_organisation&.name}\",#{log.assigned_to_id},\"#{log.assigned_to&.name}\",#{log.startdate},\"#{log.address_line1}\",\"#{log.address_line2}\",\"#{log.town_or_city}\",\"#{log.county}\",\"#{log.postcode_full}\",\"\",\"\""
end
i += 1
if (i % 100).zero?
puts "Processed #{i} lettings logs"
end
end
puts "Done #{lettings_logs_count} lettings logs"
puts "Checking LA on #{sales_logs_count} sales logs in #{year}"
i = 0
sales_logs.find_each do |log|
next unless log.valid?
log.process_postcode_changes!
unless log.save
puts "Failed to save sales log #{log.id}"
puts "Errors: #{log.errors.full_messages}"
end
if log.la.nil? && log.status == "in_progress"
puts "#sales##{log.id},\"#{log.purchid}\",#{log.owning_organisation_id},\"#{log.owning_organisation&.name}\",#{log.managing_organisation_id},\"#{log.managing_organisation&.name}\",#{log.assigned_to_id},\"#{log.assigned_to&.name}\",#{log.saledate},\"#{log.address_line1}\",\"#{log.address_line2}\",\"#{log.town_or_city}\",\"#{log.county}\",\"#{log.postcode_full}\",\"\",\"\""
end
i += 1
if (i % 100).zero?
puts "Processed #{i} sales logs"
end
end
puts "Done #{sales_logs_count} sales logs"
puts "Done"
end
desc "Parse the output of search_for_la_on_logs_with_nil_la into separate lettings and sales CSV files"
task parse_logs_moved_to_incomplete_with_no_la: :environment do
require "csv"
file = "output.txt"
lettings_headers = %w[id tenancycode propcode owning_organisation_id owning_organisation managing_organisation_id managing_organisation assigned_to_id assigned_to startdate address_line1 address_line2 town_or_city county postcode_full la_ecode la_name]
sales_headers = %w[id purchid owning_organisation_id owning_organisation managing_organisation_id managing_organisation assigned_to_id assigned_to saledate address_line1 address_line2 town_or_city county postcode_full la_ecode la_name]
lettings_csv = CSV.open("lettings_logs_moved_to_incomplete_with_no_la.csv", "w")
sales_csv = CSV.open("sales_logs_moved_to_incomplete_with_no_la.csv", "w")
lettings_csv << lettings_headers
sales_csv << sales_headers
File.readlines(file).each do |line|
line = line.strip
if line.start_with?("#lettings#")
row = CSV.parse_line(line.delete_prefix("#lettings#"), liberal_parsing: true)
lettings_csv << row
elsif line.start_with?("#sales#")
row = CSV.parse_line(line.delete_prefix("#sales#"), liberal_parsing: true)
sales_csv << row
end
end
lettings_csv.close
sales_csv.close
puts "Written lettings_logs_moved_to_incomplete_with_no_la.csv"
puts "Written sales_logs_moved_to_incomplete_with_no_la.csv"
end
desc "Split lettings and sales CSVs by managing organisation into separate files per org"
task split_logs_by_managing_org: :environment do
require "csv"
%w[lettings sales].each do |log_type|
input_file = "#{log_type}_logs_moved_to_incomplete_with_no_la.csv"
rows_by_org = Hash.new { |h, k| h[k] = [] }
table = CSV.read(input_file, headers: true)
table.each do |row|
org_name = row["managing_organisation"]
rows_by_org[org_name] << row
end
rows_by_org.each do |org_name, rows|
if rows.size < 30
puts "Skipping #{org_name} (#{rows.size} rows)"
next
end
FileUtils.mkdir_p("log_output")
sanitised_name = org_name.parameterize(separator: "_")
output_file = "log_output/#{sanitised_name}_#{log_type}_logs_moved_to_incomplete_with_no_la.csv"
CSV.open(output_file, "w") do |csv|
csv << table.headers
rows.each { |row| csv << row }
end
puts "Written #{output_file} (#{rows.size} rows)"
end
end
end
end

24
lib/tasks/remap_2025_hhregresstill_values.rake

@ -0,0 +1,24 @@
desc "Remaps hhregresstill values for manually created 2025/26 sales logs"
task :remap_2025_hhregresstill_values, %i[before_datetime] => :environment do |_task, args|
usage_message = "Usage: rake remap_2025_hhregresstill_values['before_datetime']. before_datetime must be in format YYYY-MM-DDTHH:MM:SS"
raise usage_message if args[:before_datetime].blank?
before_datetime = Time.zone.parse(args[:before_datetime])
raise usage_message if before_datetime.nil?
logs = SalesLog.filter_by_year(2025).where(bulk_upload_id: nil).where(hhregresstill: [5, 6, 7]).where("created_at < ?", before_datetime)
puts "Updating #{logs.count} sales logs"
updated_ids = []
logs.find_each do |log|
new_value = case log.hhregresstill
when 5 then 10
when 6, 7 then 9
end
log.update!(hhregresstill: new_value)
updated_ids << log.id
end
puts "Updated log IDs: #{updated_ids.join(', ')}"
puts "Done"
end

11
lib/tasks/round_value_for_2026_sales_logs.rake

@ -0,0 +1,11 @@
desc "Rounds purchase price (the 'value' field) for sales logs in the database if not a whole number"
task round_value_for_2026_sales_logs: :environment do
logs = SalesLog.filter_by_year(2026).where("value % 1 != 0")
puts "Correcting #{logs.count} sales logs, #{logs.map(&:id)}"
logs.find_each do |log|
log.update(value: log.value.round)
end
puts "Done"
end

10
lib/tasks/update_logs_with_invalid_hb_benefits_2026.rake

@ -0,0 +1,10 @@
desc "For logs that fail the validate_housing_universal_credit_matches_income_proportion check created before we released it, clear the answer to the question"
task update_logs_with_invalid_hb_benefits_2026: :environment do
impacted_logs = LettingsLog.filter_by_year(2026).where(hb: [1, 6], benefits: 3)
puts "#{impacted_logs.count} logs will be updated #{impacted_logs.map(&:id)}"
impacted_logs.update!(benefits: nil, hb: nil)
puts "Done"
end

2
package.json

@ -2,7 +2,7 @@
"name": "data-collector", "name": "data-collector",
"private": true, "private": true,
"engines": { "engines": {
"node": "^20.0.0" "node": "^24.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.17.7", "@babel/core": "^7.17.7",

111
spec/components/data_protection_confirmation_banner_component_spec.rb

@ -23,13 +23,25 @@ RSpec.describe DataProtectionConfirmationBannerComponent, type: :component do
organisation.users.where(is_dpo: true).destroy_all organisation.users.where(is_dpo: true).destroy_all
end end
it "displays the banner" do context "when org does not have a signed data sharing agreement" do
expect(component.display_banner?).to be(true) let(:organisation) { create(:organisation, :without_dpc) }
expect(render).to have_link( let(:user) { create(:user, organisation:, with_dsa: false) }
"Contact helpdesk to assign a data protection officer",
href: "https://mhclgdigital.atlassian.net/servicedesk/customer/portal/6/group/11", it "displays the banner" do
) expect(component.display_banner?).to be(true)
expect(render).to have_selector("p", text: "To create logs your organisation must state a data protection officer. They must sign the Data Sharing Agreement.") expect(render).to have_link(
"Contact helpdesk to assign a data protection officer",
href: "https://mhclgdigital.atlassian.net/servicedesk/customer/portal/6/group/11",
)
expect(render).to have_selector("p", text: "To create logs your organisation must state a data protection officer. They must sign the Data Sharing Agreement.")
end
end
context "when org does have a signed data sharing agreement" do
it "does not display banner" do
expect(component.display_banner?).to be(false)
expect(render.content).to be_empty
end
end end
end end
@ -81,7 +93,7 @@ RSpec.describe DataProtectionConfirmationBannerComponent, type: :component do
end end
end end
context "when org has a signed data sharing agremeent" do context "when org has a signed data sharing agreement" do
it "does not display banner" do it "does not display banner" do
expect(component.display_banner?).to be(false) expect(component.display_banner?).to be(false)
expect(render.content).to be_empty expect(render.content).to be_empty
@ -121,88 +133,5 @@ RSpec.describe DataProtectionConfirmationBannerComponent, type: :component do
end end
end end
end end
context "when org does not have a DPO" do
before do
organisation.users.where(is_dpo: true).destroy_all
end
it "displays the banner" do
expect(component.display_banner?).to be(true)
expect(render).to have_link(
"Contact helpdesk to assign a data protection officer",
href: "https://mhclgdigital.atlassian.net/servicedesk/customer/portal/6/group/11",
)
expect(render).to have_selector("p", text: "To create logs your organisation must state a data protection officer. They must sign the Data Sharing Agreement.")
end
end
context "when org has a DPO" do
context "when org does not have a signed data sharing agreement" do
context "when user is not a DPO" do
let(:organisation) { create(:organisation, :without_dpc) }
let(:user) { create(:user, organisation:, with_dsa: false) }
let!(:dpo) { create(:user, :data_protection_officer, organisation:, with_dsa: false) }
it "displays the banner and shows DPOs" do
expect(component.display_banner?).to be(true)
expect(render.css("a")).to be_empty
expect(render).to have_selector("p", text: "Your data protection officer must accept the Data Sharing Agreement on CORE before you can create any logs.")
expect(render).to have_selector("p", text: "You can ask: #{dpo.name}")
end
context "and has a parent organisation that owns stock and has signed DSA" do
before do
parent_organisation = create(:organisation, holds_own_stock: true)
create(:organisation_relationship, child_organisation: organisation, parent_organisation:)
end
it "displays the banner and shows DPOs" do
expect(component.display_banner?).to be(true)
expect(render.css("a")).to be_empty
expect(render).to have_selector("p", text: "Your data protection officer must accept the Data Sharing Agreement on CORE before you can create any logs.")
expect(render).to have_selector("p", text: "You can ask: #{dpo.name}")
end
end
end
context "when user is a DPO" do
let(:organisation) { create(:organisation, :without_dpc) }
let(:user) { create(:user, :data_protection_officer, organisation:, with_dsa: false) }
it "displays the banner and asks to sign" do
expect(component.display_banner?).to be(true)
expect(render).to have_link(
"Read the Data Sharing Agreement",
href: "/organisations/#{organisation.id}/data-sharing-agreement",
)
expect(render).to have_selector("p", text: "Your organisation must accept the Data Sharing Agreement before you can create any logs.")
end
context "and has a parent organisation that owns stock and has signed DSA" do
before do
parent_organisation = create(:organisation, holds_own_stock: true)
create(:organisation_relationship, child_organisation: organisation, parent_organisation:)
end
it "displays the banner and asks to sign" do
expect(component.display_banner?).to be(true)
expect(render).to have_link(
"Read the Data Sharing Agreement",
href: "/organisations/#{organisation.id}/data-sharing-agreement",
)
expect(render).to have_selector("p", text: "Your organisation must accept the Data Sharing Agreement before you can create any logs.")
end
end
end
end
context "when org has a signed data sharing agremeent" do
it "does not display banner" do
expect(component.display_banner?).to be(false)
expect(render.content).to be_empty
end
end
end
end end
end end

9
spec/features/form/progressive_total_field_spec.rb

@ -58,4 +58,13 @@ RSpec.describe "Accessible Autocomplete" do
fill_in("lettings-log-supcharg-field-error", with: 50) fill_in("lettings-log-supcharg-field-error", with: 50)
expect(find("#lettings-log-tcharge-field").value).to eq("550.00") expect(find("#lettings-log-tcharge-field").value).to eq("550.00")
end end
it "does not show 'NaN' if one of the inputs is not a number", :js do
visit("/lettings-logs/#{lettings_log.id}/rent")
expect(page).to have_selector("#tcharge_div")
fill_in("lettings-log-brent-field", with: 5)
expect(find("#lettings-log-tcharge-field").value).to eq("5.00")
fill_in("lettings-log-pscharge-field", with: "something else")
expect(find("#lettings-log-tcharge-field").value).to eq("5.00")
end
end end

20
spec/features/user_spec.rb

@ -282,6 +282,12 @@ RSpec.describe "User Features" do
end end
end end
end end
it "shows correct filters" do
expect(page).to have_selector("label", text: "Data provider")
expect(page).to have_selector("label", text: "Data coordinator")
expect(page).not_to have_selector("label", text: "Support")
end
end end
end end
@ -619,6 +625,20 @@ RSpec.describe "User Features" do
expect(page).to have_button("Resend invite link") expect(page).to have_button("Resend invite link")
end end
end end
context "when filtering users" do
before do
allow(user).to receive(:need_two_factor_authentication?).and_return(false)
sign_in(user)
visit(users_path)
end
it "shows correct filters" do
expect(page).to have_selector("label", text: "Data provider")
expect(page).to have_selector("label", text: "Data coordinator")
expect(page).to have_selector("label", text: "Support")
end
end
end end
context "when the user is a customer support person" do context "when the user is a customer support person" do

2
spec/fixtures/files/lettings_log_csv_export_labels_23.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/lettings_log_csv_export_labels_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/lettings_log_csv_export_labels_25.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/lettings_log_csv_export_non_support_labels_23.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/lettings_log_csv_export_non_support_labels_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/lettings_log_csv_export_non_support_labels_25.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_labels_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_labels_25.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_labels_26.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_non_support_labels_25.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv vendored

File diff suppressed because one or more lines are too long

72
spec/models/form/lettings/pages/property_local_authority_spec.rb

@ -35,66 +35,26 @@ RSpec.describe Form::Lettings::Pages::PropertyLocalAuthority, type: :model do
context "when routing to the page" do context "when routing to the page" do
let(:log) { build(:lettings_log) } let(:log) { build(:lettings_log) }
context "with form before 2024" do before do
before do allow(form).to receive(:start_year_2025_or_later?).and_return(true)
allow(form).to receive(:start_year_2024_or_later?).and_return(false)
end
it "is routed to when la is not inferred and it is general needs log" do
log.needstype = 1
log.is_la_inferred = false
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when la is inferred" do
log.needstype = 1
log.is_la_inferred = true
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when it's a supported housing log" do
log.needstype = 2
log.is_la_inferred = false
expect(page).not_to be_routed_to(log, nil)
end
end end
context "with form after 2024" do it "is routed to when la is not inferred and it is general needs log" do
before do log.needstype = 1
allow(form).to receive(:start_year_2024_or_later?).and_return(true) log.is_la_inferred = false
end expect(page).to be_routed_to(log, nil)
end
it "is routed to when la is not inferred, it is general needs log and address search has been given" do
log.needstype = 1
log.is_la_inferred = false
log.address_line1_input = "1"
log.postcode_full_input = "A11AA"
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when la is inferred" do
log.needstype = 1
log.is_la_inferred = true
log.address_line1_input = "1"
log.postcode_full_input = "A11AA"
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when it's a supported housing log" do it "is not routed to when la is inferred" do
log.needstype = 2 log.needstype = 1
log.is_la_inferred = false log.is_la_inferred = true
log.address_line1_input = "1" expect(page).not_to be_routed_to(log, nil)
log.postcode_full_input = "A11AA" end
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when address search is not given" do it "is not routed to when it's a supported housing log" do
log.needstype = 1 log.needstype = 2
log.is_la_inferred = false log.is_la_inferred = false
log.address_line1_input = nil expect(page).not_to be_routed_to(log, nil)
log.postcode_full_input = "A11AA"
expect(page).not_to be_routed_to(log, nil)
end
end end
end end
end end

26
spec/models/form/lettings/questions/location_id_spec.rb

@ -142,27 +142,27 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do
context "and some locations start with numbers" do context "and some locations start with numbers" do
before do before do
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 5), name: "2 Abe Road") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 5), name: "2 Abe Road", postcode: "AA1 1AA")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 6), name: "1 Abe Road") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 6), name: "1 Abe Road", postcode: "AA1 2AA")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 7), name: "1 Lake Lane") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 7), name: "1 Lake Lane", postcode: "AA1 3AA")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 8), name: "3 Abe Road") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 8), name: "3 Abe Road", postcode: "AA1 4AA")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 9), name: "2 Lake Lane") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 9), name: "2 Lake Lane", postcode: "AA1 5AA")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 10), name: "Smith Avenue") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 10), name: "Smith Avenue", postcode: "AA1 6AA")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 11), name: "Abacus Road") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 11), name: "Abacus Road", postcode: "AA1 7AA")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12), name: "Hawthorne Road") FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12), name: "Hawthorne Road", postcode: "AA1 8AA")
lettings_log.update!(scheme:) lettings_log.update!(scheme:)
end end
it "orders the locations by name then numerically" do it "orders the locations by postcode" do
expect(question.displayed_answer_options(lettings_log).values.map { |v| v["hint"] }).to eq([ expect(question.displayed_answer_options(lettings_log).values.map { |v| v["hint"] }).to eq([
"Abacus Road",
"1 Abe Road",
"2 Abe Road", "2 Abe Road",
"3 Abe Road", "1 Abe Road",
"Hawthorne Road",
"1 Lake Lane", "1 Lake Lane",
"3 Abe Road",
"2 Lake Lane", "2 Lake Lane",
"Smith Avenue", "Smith Avenue",
"Abacus Road",
"Hawthorne Road",
]) ])
end end
end end

49
spec/models/form/sales/pages/property_building_type_spec.rb

@ -1,12 +1,15 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Pages::PropertyBuildingType, type: :model do RSpec.describe Form::Sales::Pages::PropertyBuildingType, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil } let(:page_id) { nil }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1)) } let(:form) { instance_double(Form, start_date: current_collection_start_date) }
let(:subsection) { instance_double(Form::Subsection, enabled?: true, form:) } let(:subsection) { instance_double(Form::Subsection, enabled?: true, form:) }
let(:saledate) { current_collection_start_date }
it "has correct subsection" do it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
@ -24,45 +27,21 @@ RSpec.describe Form::Sales::Pages::PropertyBuildingType, type: :model do
expect(page.description).to be_nil expect(page.description).to be_nil
end end
context "with form year 2024" do context "with a staircasing log" do
let(:form) { Form.new(nil, 2024, [], "sales") } let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
let(:saledate) { Time.zone.local(2024, 4, 1) } let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 1, saledate:) }
context "with a staircasing log" do
let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 1, saledate:) }
it "is routed to" do
expect(page.routed_to?(log, nil)).to be true
end
end
context "with a non-staircasing log" do
let(:log) { build(:sales_log, staircase: nil, saledate:) }
it "is routed to" do it "is not routed to" do
expect(page.routed_to?(log, nil)).to be true expect(page.routed_to?(log, nil)).to be false
end
end end
end end
context "with form year 2025" do context "with a non-staircasing log" do
let(:form) { Form.new(nil, 2025, [], "sales") } let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
let(:saledate) { Time.zone.local(2025, 4, 1) } let(:log) { build(:sales_log, staircase: nil, saledate:) }
context "with a staircasing log" do
let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 1, saledate:) }
it "is not routed to" do
expect(page.routed_to?(log, nil)).to be false
end
end
context "with a non-staircasing log" do
let(:log) { build(:sales_log, staircase: nil, saledate:) }
it "is routed to" do it "is routed to" do
expect(page.routed_to?(log, nil)).to be true expect(page.routed_to?(log, nil)).to be true
end
end end
end end
end end

61
spec/models/form/sales/pages/property_local_authority_spec.rb

@ -17,18 +17,8 @@ RSpec.describe Form::Sales::Pages::PropertyLocalAuthority, type: :model do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
end end
describe "has correct questions" do it "has correct questions" do
context "when 2023" do expect(page.questions.map(&:id)).to eq(%w[la])
let(:start_date) { Time.utc(2023, 2, 8) }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(
%w[
la
],
)
end
end
end end
it "has the correct id" do it "has the correct id" do
@ -42,47 +32,18 @@ RSpec.describe Form::Sales::Pages::PropertyLocalAuthority, type: :model do
context "when routing to the page" do context "when routing to the page" do
let(:log) { build(:sales_log) } let(:log) { build(:sales_log) }
context "with form before 2024" do before do
before do allow(form).to receive(:start_year_2025_or_later?).and_return(true)
allow(form).to receive(:start_year_2024_or_later?).and_return(false)
end
it "is routed to when la is not inferred" do
log.is_la_inferred = false
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when la is inferred" do
log.is_la_inferred = true
expect(page).not_to be_routed_to(log, nil)
end
end end
context "with form after 2024" do it "is routed to when la is not inferred" do
before do log.is_la_inferred = false
allow(form).to receive(:start_year_2024_or_later?).and_return(true) expect(page).to be_routed_to(log, nil)
end end
it "is routed to when la is not inferred and address search has been given" do
log.is_la_inferred = false
log.address_line1_input = "1"
log.postcode_full_input = "A11AA"
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when la is inferred" do
log.is_la_inferred = true
log.address_line1_input = "1"
log.postcode_full_input = "A11AA"
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when address search is not given" do it "is not routed to when la is inferred" do
log.is_la_inferred = false log.is_la_inferred = true
log.address_line1_input = nil expect(page).not_to be_routed_to(log, nil)
log.postcode_full_input = "A11AA"
expect(page).not_to be_routed_to(log, nil)
end
end end
end end
end end

49
spec/models/form/sales/pages/property_wheelchair_accessible_spec.rb

@ -1,12 +1,15 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Pages::PropertyWheelchairAccessible, type: :model do RSpec.describe Form::Sales::Pages::PropertyWheelchairAccessible, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil } let(:page_id) { nil }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:form) { instance_double(Form, start_year_2024_or_later?: false, start_date: Time.zone.local(2023, 4, 1)) } let(:form) { instance_double(Form, start_year_2024_or_later?: true, start_date: current_collection_start_date) }
let(:subsection) { instance_double(Form::Subsection, enabled?: true, form:) } let(:subsection) { instance_double(Form::Subsection, enabled?: true, form:) }
let(:saledate) { current_collection_start_date }
it "has correct subsection" do it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
@ -24,45 +27,21 @@ RSpec.describe Form::Sales::Pages::PropertyWheelchairAccessible, type: :model do
expect(page.description).to be_nil expect(page.description).to be_nil
end end
context "with form year 2024" do context "with a staircasing log" do
let(:form) { Form.new(nil, 2024, [], "sales") } let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
let(:saledate) { Time.zone.local(2024, 4, 1) } let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 1, saledate:) }
context "with a staircasing log" do
let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 1, saledate:) }
it "is routed to" do
expect(page.routed_to?(log, nil)).to be true
end
end
context "with a non-staircasing log" do
let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 2, saledate:) }
it "is routed to" do it "is not routed to" do
expect(page.routed_to?(log, nil)).to be true expect(page.routed_to?(log, nil)).to be false
end
end end
end end
context "with form year 2025" do context "with a non-staircasing log" do
let(:form) { Form.new(nil, 2025, [], "sales") } let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
let(:saledate) { Time.zone.local(2025, 4, 1) } let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 2, saledate:) }
context "with a staircasing log" do
let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 1, saledate:) }
it "is not routed to" do
expect(page.routed_to?(log, nil)).to be false
end
end
context "with a non-staircasing log" do
let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 2, saledate:) }
it "is routed to" do it "is routed to" do
expect(page.routed_to?(log, nil)).to be true expect(page.routed_to?(log, nil)).to be true
end
end end
end end
end end

27
spec/models/form/sales/questions/buyer_still_serving_spec.rb

@ -32,10 +32,21 @@ RSpec.describe Form::Sales::Questions::BuyerStillServing, type: :model do
it "has the correct answer_options" do it "has the correct answer_options" do
expect(question.answer_options).to eq({ expect(question.answer_options).to eq({
"4" => { "value" => "Yes" }, "4" => { "value" => "Yes" },
"5" => { "value" => "No" }, "5" => { "value" => "No - they left up to and including 2 years ago" },
"6" => { "value" => "Buyer prefers not to say" }, "6" => { "value" => "No - they left more than 2 years ago" },
"divider" => { "value" => true }, "divider" => { "value" => true },
"7" => { "value" => "Don’t know" }, "9" => { "value" => "Don’t know" },
"10" => { "value" => "No" },
})
end
it "has the correct displayed_answer_options" do
expect(question.displayed_answer_options(nil)).to eq({
"4" => { "value" => "Yes" },
"5" => { "value" => "No - they left up to and including 2 years ago" },
"6" => { "value" => "No - they left more than 2 years ago" },
"divider" => { "value" => true },
"9" => { "value" => "Don’t know" },
}) })
end end
end end
@ -52,5 +63,15 @@ RSpec.describe Form::Sales::Questions::BuyerStillServing, type: :model do
"9" => { "value" => "Don’t know" }, "9" => { "value" => "Don’t know" },
}) })
end end
it "has the correct displayed_answer_options" do
expect(question.displayed_answer_options(nil)).to eq({
"4" => { "value" => "Yes" },
"5" => { "value" => "No - they left up to and including 2 years ago" },
"6" => { "value" => "No - they left more than 2 years ago" },
"divider" => { "value" => true },
"9" => { "value" => "Don’t know" },
})
end
end end
end end

2
spec/models/form/sales/questions/property_building_type_spec.rb

@ -25,7 +25,7 @@ RSpec.describe Form::Sales::Questions::PropertyBuildingType, type: :model do
it "has the correct answer_options" do it "has the correct answer_options" do
expect(question.answer_options).to eq({ expect(question.answer_options).to eq({
"1" => { "value" => "Purpose built" }, "1" => { "value" => "Purpose-built" },
"2" => { "value" => "Converted from previous residential or non-residential property" }, "2" => { "value" => "Converted from previous residential or non-residential property" },
}) })
end end

39
spec/models/lettings_log_derived_fields_spec.rb

@ -111,45 +111,6 @@ RSpec.describe LettingsLog, type: :model do
end end
describe "deriving household member fields" do describe "deriving household member fields" do
context "when it is 2024", metadata: { year: 24 } do
let(:startdate) { collection_start_date_for_year(2024) }
before do
log.assign_attributes(
relat2: "X",
relat3: "C",
relat4: "X",
relat5: "C",
relat7: "C",
relat8: "X",
age1: 22,
age2: 16,
age4: 60,
age6: 88,
age7: 14,
age8: 42,
)
log.set_derived_fields!
end
it "correctly derives totchild" do
expect(log.totchild).to eq 3
end
it "correctly derives totelder" do
expect(log.totelder).to eq 2
end
it "correctly derives totadult" do
expect(log.totadult).to eq 3
end
it "correctly derives economic status for tenants under 16" do
expect(log.ecstat7).to eq 9
end
end
context "when it is 2025", metadata: { year: 25 } do context "when it is 2025", metadata: { year: 25 } do
let(:startdate) { collection_start_date_for_year(2025) } let(:startdate) { collection_start_date_for_year(2025) }

104
spec/models/validations/date_validations_spec.rb

@ -1,6 +1,8 @@
require "rails_helper" require "rails_helper"
RSpec.describe Validations::DateValidations do RSpec.describe Validations::DateValidations do
include CollectionTimeHelper
subject(:date_validator) { validator_class.new } subject(:date_validator) { validator_class.new }
let(:validator_class) { Class.new { include Validations::DateValidations } } let(:validator_class) { Class.new { include Validations::DateValidations } }
@ -54,44 +56,22 @@ RSpec.describe Validations::DateValidations do
expect(record.errors["mrcdate"]).to be_empty expect(record.errors["mrcdate"]).to be_empty
end end
context "with 2024 logs or earlier" do it "cannot be more than 20 years before the tenancy start date" do
it "cannot be more than 10 years before the tenancy start date" do record.startdate = current_collection_start_date
record.startdate = Time.zone.local(2024, 4, 1) record.mrcdate = current_collection_start_date - 20.years - 1.day
record.mrcdate = Time.zone.local(2014, 1, 31) date_validator.validate_property_major_repairs(record)
date_validator.validate_property_major_repairs(record) expect(record.errors["mrcdate"])
expect(record.errors["mrcdate"]) .to include(match I18n.t("validations.lettings.date.mrcdate.twenty_years_before_tenancy_start"))
.to include(match I18n.t("validations.lettings.date.mrcdate.ten_years_before_tenancy_start")) expect(record.errors["startdate"])
expect(record.errors["startdate"]) .to include(match I18n.t("validations.lettings.date.startdate.twenty_years_after_mrc_date"))
.to include(match I18n.t("validations.lettings.date.startdate.ten_years_after_mrc_date"))
end
it "must be within 10 years of the tenancy start date" do
record.startdate = Time.zone.local(2024, 2, 1)
record.mrcdate = Time.zone.local(2014, 2, 1)
date_validator.validate_property_major_repairs(record)
expect(record.errors["mrcdate"]).to be_empty
expect(record.errors["startdate"]).to be_empty
end
end end
context "with 2025 logs or later" do it "can be within 20 years of the tenancy start date" do
it "cannot be more than 20 years before the tenancy start date" do record.startdate = current_collection_start_date
record.startdate = Time.zone.local(2026, 2, 1) record.mrcdate = current_collection_start_date - 20.years
record.mrcdate = Time.zone.local(2006, 1, 31) date_validator.validate_property_major_repairs(record)
date_validator.validate_property_major_repairs(record) expect(record.errors["mrcdate"]).to be_empty
expect(record.errors["mrcdate"]) expect(record.errors["startdate"]).to be_empty
.to include(match I18n.t("validations.lettings.date.mrcdate.twenty_years_before_tenancy_start"))
expect(record.errors["startdate"])
.to include(match I18n.t("validations.lettings.date.startdate.twenty_years_after_mrc_date"))
end
it "must be within 20 years of the tenancy start date" do
record.startdate = Time.zone.local(2026, 2, 1)
record.mrcdate = Time.zone.local(2006, 2, 1)
date_validator.validate_property_major_repairs(record)
expect(record.errors["mrcdate"]).to be_empty
expect(record.errors["startdate"]).to be_empty
end
end end
context "when reason for vacancy is first let of property" do context "when reason for vacancy is first let of property" do
@ -148,45 +128,23 @@ RSpec.describe Validations::DateValidations do
expect(record.errors["voiddate"]).to be_empty expect(record.errors["voiddate"]).to be_empty
end end
context "with 2024 logs or earlier" do it "cannot be more than 20 years before the tenancy start date" do
it "cannot be more than 10 years before the tenancy start date" do record.startdate = current_collection_start_date
record.startdate = Time.zone.local(2024, 4, 1) record.voiddate = current_collection_start_date - 20.years - 1.day
record.voiddate = Time.zone.local(2014, 1, 31) date_validator.validate_property_void_date(record)
date_validator.validate_property_void_date(record) date_validator.validate_startdate(record)
expect(record.errors["voiddate"]) expect(record.errors["voiddate"])
.to include(match I18n.t("validations.lettings.date.void_date.ten_years_before_tenancy_start")) .to include(match I18n.t("validations.lettings.date.void_date.twenty_years_before_tenancy_start"))
expect(record.errors["startdate"]) expect(record.errors["startdate"])
.to include(match I18n.t("validations.lettings.date.startdate.ten_years_after_void_date")) .to include(match I18n.t("validations.lettings.date.startdate.twenty_years_after_void_date"))
end
it "must be within 10 years of the tenancy start date" do
record.startdate = Time.zone.local(2024, 2, 1)
record.voiddate = Time.zone.local(2014, 2, 1)
date_validator.validate_property_void_date(record)
expect(record.errors["voiddate"]).to be_empty
expect(record.errors["startdate"]).to be_empty
end
end end
context "with 2025 logs or later" do it "can be within 20 years of the tenancy start date" do
it "cannot be more than 20 years before the tenancy start date" do record.startdate = current_collection_start_date
record.startdate = Time.zone.local(2026, 2, 1) record.voiddate = current_collection_start_date - 20.years
record.voiddate = Time.zone.local(2006, 1, 31) date_validator.validate_property_void_date(record)
date_validator.validate_property_void_date(record) expect(record.errors["voiddate"]).to be_empty
date_validator.validate_startdate(record) expect(record.errors["startdate"]).to be_empty
expect(record.errors["voiddate"])
.to include(match I18n.t("validations.lettings.date.void_date.twenty_years_before_tenancy_start"))
expect(record.errors["startdate"])
.to include(match I18n.t("validations.lettings.date.startdate.twenty_years_after_void_date"))
end
it "must be within 20 years of the tenancy start date" do
record.startdate = Time.zone.local(2026, 2, 1)
record.voiddate = Time.zone.local(2006, 2, 1)
date_validator.validate_property_void_date(record)
expect(record.errors["voiddate"]).to be_empty
expect(record.errors["startdate"]).to be_empty
end
end end
context "when major repairs have been carried out" do context "when major repairs have been carried out" do

101
spec/models/validations/financial_validations_spec.rb

@ -5,11 +5,6 @@ RSpec.describe Validations::FinancialValidations do
let(:validator_class) { Class.new { include Validations::FinancialValidations } } let(:validator_class) { Class.new { include Validations::FinancialValidations } }
let(:record) { FactoryBot.create(:lettings_log) } let(:record) { FactoryBot.create(:lettings_log) }
let(:fake_2021_2022_form) { Form.new("spec/fixtures/forms/2021_2022.json") }
before do
allow(FormHandler.instance).to receive(:current_lettings_form).and_return(fake_2021_2022_form)
end
describe "earnings and income frequency" do describe "earnings and income frequency" do
it "when earnings are provided it validates that income frequency must be provided" do it "when earnings are provided it validates that income frequency must be provided" do
@ -1234,4 +1229,100 @@ RSpec.describe Validations::FinancialValidations do
end end
end end
end end
describe "universal credit and income sources validations" do
before do
record.hb = hb
record.benefits = benefits
end
context "with a 2025 form", metadata: { year: 25 } do
before do
allow(record.form).to receive(:start_year_2026_or_later?).and_return(false)
end
context "when tenant receives universal credit and no household income comes from benefits" do
let(:hb) { 6 }
let(:benefits) { 3 }
it "does not add errors" do
financial_validator.validate_housing_benefits_matches_income_proportion(record)
expect(record.errors["hb"]).to be_empty
expect(record.errors["benefits"]).to be_empty
end
end
end
context "with a 2026 form", metadata: { year: 26 } do
before do
allow(record.form).to receive(:start_year_2026_or_later?).and_return(true)
end
context "when tenant receives universal credit and no household income comes from benefits" do
let(:hb) { 6 }
let(:benefits) { 3 }
it "adds errors to hb and benefits" do
financial_validator.validate_housing_benefits_matches_income_proportion(record)
expect(record.errors["hb"]).to include(match I18n.t("validations.lettings.financial.hb.housing_benefits_not_match_income_source"))
expect(record.errors["benefits"]).to include(match I18n.t("validations.lettings.financial.benefits.housing_benefits_not_match_income_source"))
end
end
context "when tenant receives universal credit and some household income comes from benefits" do
let(:hb) { 6 }
let(:benefits) { 2 }
it "does not add errors" do
financial_validator.validate_housing_benefits_matches_income_proportion(record)
expect(record.errors["hb"]).to be_empty
expect(record.errors["benefits"]).to be_empty
end
end
context "when tenant receives housing benefit and no household income comes from benefits" do
let(:hb) { 1 }
let(:benefits) { 3 }
it "adds errors to hb and benefits" do
financial_validator.validate_housing_benefits_matches_income_proportion(record)
expect(record.errors["hb"]).to include(match I18n.t("validations.lettings.financial.hb.housing_benefits_not_match_income_source"))
expect(record.errors["benefits"]).to include(match I18n.t("validations.lettings.financial.benefits.housing_benefits_not_match_income_source"))
end
end
context "when tenant receives housing benefit and some household income comes from benefits" do
let(:hb) { 1 }
let(:benefits) { 2 }
it "does not add errors" do
financial_validator.validate_housing_benefits_matches_income_proportion(record)
expect(record.errors["hb"]).to be_empty
expect(record.errors["benefits"]).to be_empty
end
end
context "when hb is not set" do
let(:hb) { nil }
let(:benefits) { 3 }
it "does not add errors" do
financial_validator.validate_housing_benefits_matches_income_proportion(record)
expect(record.errors["hb"]).to be_empty
expect(record.errors["benefits"]).to be_empty
end
end
context "when benefits is not set" do
let(:hb) { 6 }
let(:benefits) { nil }
it "does not add errors" do
financial_validator.validate_housing_benefits_matches_income_proportion(record)
expect(record.errors["hb"]).to be_empty
expect(record.errors["benefits"]).to be_empty
end
end
end
end
end end

51
spec/models/validations/sales/soft_validations_spec.rb

@ -30,57 +30,8 @@ RSpec.describe Validations::Sales::SoftValidations do
expect(record).not_to be_income2_outside_soft_range_for_ecstat expect(record).not_to be_income2_outside_soft_range_for_ecstat
end end
context "when log year is before 2025" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2024, 12, 25)) }
it "does not trigger for low income1 if ecstat1 has no soft min" do
record.income1 = 50
record.ecstat1 = 4
expect(record).not_to be_income1_outside_soft_range_for_ecstat
end
it "returns true if income1 is below soft min for ecstat1" do
record.income1 = 4500
record.ecstat1 = 1
expect(record).to be_income1_outside_soft_range_for_ecstat
end
it "returns false if income1 is >= soft min for ecstat1" do
record.income1 = 1500
record.ecstat1 = 2
expect(record).not_to be_income1_outside_soft_range_for_ecstat
end
it "does not trigger for income2 if ecstat2 has no soft min" do
record.income2 = 50
record.ecstat2 = 8
expect(record).not_to be_income2_outside_soft_range_for_ecstat
end
it "returns true if income2 is below soft min for ecstat2" do
record.income2 = 999
record.ecstat2 = 3
expect(record).to be_income2_outside_soft_range_for_ecstat
end
it "returns false if income2 is >= soft min for ecstat2" do
record.income2 = 2500
record.ecstat2 = 5
expect(record).not_to be_income2_outside_soft_range_for_ecstat
end
it "does not trigger for being over maxima" do
record.ecstat1 = 1
record.income1 = 200_000
record.ecstat2 = 2
record.income2 = 100_000
expect(record).not_to be_income1_outside_soft_range_for_ecstat
expect(record).not_to be_income2_outside_soft_range_for_ecstat
end
end
context "when log year is 2025" do context "when log year is 2025" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2025, 12, 25)) } let(:record) { build(:sales_log, saledate: collection_start_date_for_year(2025)) }
it "returns true if income1 is below soft min for ecstat1" do it "returns true if income1 is below soft min for ecstat1" do
record.income1 = 13_399 record.income1 = 13_399

2
spec/requests/check_errors_controller_spec.rb

@ -84,7 +84,7 @@ RSpec.describe CheckErrorsController, type: :request do
end end
it "displays correct clear and change links" do it "displays correct clear and change links" do
expect(page.all(:button, value: "Clear").count).to eq(1) expect(page.all(:button, value: "Clear").count).to eq(2)
expect(page).to have_link("Change", count: 1) expect(page).to have_link("Change", count: 1)
expect(page).to have_button("Clear all") expect(page).to have_button("Clear all")
end end

6
spec/requests/collection_resources_controller_spec.rb

@ -734,7 +734,7 @@ RSpec.describe CollectionResourcesController, type: :request do
end end
describe "GET #edit_additional_collection_resource" do describe "GET #edit_additional_collection_resource" do
let(:collection_resource) { create(:collection_resource, :additional, year: 2025, log_type: "sales", short_display_name: "additional resource", download_filename: "additional.pdf") } let(:collection_resource) { create(:collection_resource, :additional, year: current_collection_start_year, log_type: "sales", short_display_name: "additional resource", download_filename: "additional.pdf") }
context "when user is not signed in" do context "when user is not signed in" do
it "redirects to the sign in page" do it "redirects to the sign in page" do
@ -773,7 +773,7 @@ RSpec.describe CollectionResourcesController, type: :request do
let(:user) { create(:user, :support) } let(:user) { create(:user, :support) }
before do before do
allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 1, 8)) allow(Time.zone).to receive(:today).and_return(current_collection_after_crossover_start_date)
allow(user).to receive(:need_two_factor_authentication?).and_return(false) allow(user).to receive(:need_two_factor_authentication?).and_return(false)
sign_in user sign_in user
end end
@ -786,7 +786,7 @@ RSpec.describe CollectionResourcesController, type: :request do
it "displays update collection resources page content" do it "displays update collection resources page content" do
get collection_resource_edit_path(collection_resource) get collection_resource_edit_path(collection_resource)
expect(page).to have_content("Sales 2025 to 2026") expect(page).to have_content("Sales #{current_collection_start_year} to #{current_collection_end_year}")
expect(page).to have_content("Change the additional resource") expect(page).to have_content("Change the additional resource")
expect(page).to have_content("This file will be available for all users to download.") expect(page).to have_content("This file will be available for all users to download.")
expect(page).to have_content("Upload file") expect(page).to have_content("Upload file")

197
spec/services/csv/lettings_log_csv_service_spec.rb

@ -2,6 +2,8 @@ require "rails_helper"
require "rake" require "rake"
RSpec.describe Csv::LettingsLogCsvService do RSpec.describe Csv::LettingsLogCsvService do
include CollectionTimeHelper
subject(:task) { Rake::Task["data_import:add_variable_definitions"] } subject(:task) { Rake::Task["data_import:add_variable_definitions"] }
before do before do
@ -16,7 +18,7 @@ RSpec.describe Csv::LettingsLogCsvService do
let(:user) { create(:user, :support, email: "s.port@jeemayle.com") } let(:user) { create(:user, :support, email: "s.port@jeemayle.com") }
let(:service) { described_class.new(user:, export_type:, year:) } let(:service) { described_class.new(user:, export_type:, year:) }
let(:export_type) { "labels" } let(:export_type) { "labels" }
let(:year) { 2024 } let(:year) { current_collection_start_year }
let(:csv) { CSV.parse(service.prepare_csv(LettingsLog.where(id: logs.map(&:id)))) } let(:csv) { CSV.parse(service.prepare_csv(LettingsLog.where(id: logs.map(&:id)))) }
let(:logs) { [log] } let(:logs) { [log] }
let(:definition_line) { csv.first } let(:definition_line) { csv.first }
@ -582,199 +584,6 @@ RSpec.describe Csv::LettingsLogCsvService do
end end
end end
end end
context "when the requested log year is 2024" do
let(:year) { 2024 }
let(:organisation) { create(:organisation, provider_type: "LA", name: "MHCLG") }
let(:log) do
create(
:lettings_log,
:ignore_validation_errors,
created_by: user,
assigned_to: user,
created_at: Time.zone.local(2024, 4, 1),
updated_at: Time.zone.local(2024, 4, 1),
owning_organisation: organisation,
managing_organisation: organisation,
needstype: 1,
renewal: 0,
startdate: Time.zone.local(2024, 4, 1),
rent_type: 1,
tenancycode: "HIJKLMN",
propcode: "ABCDEFG",
declaration: 1,
address_line1: "Address line 1",
town_or_city: "London",
postcode_full: "NW9 5LL",
la: "E09000003",
is_la_inferred: false,
address_line1_as_entered: "address line 1 as entered",
address_line2_as_entered: "address line 2 as entered",
town_or_city_as_entered: "town or city as entered",
county_as_entered: "county as entered",
postcode_full_as_entered: "AB1 2CD",
la_as_entered: "la as entered",
first_time_property_let_as_social_housing: 0,
unitletas: 2,
rsnvac: 6,
unittype_gn: 7,
builtype: 1,
wchair: 1,
beds: 3,
voiddate: Time.zone.local(2024, 3, 30),
majorrepairs: 1,
mrcdate: Time.zone.local(2024, 3, 31),
joint: 3,
startertenancy: 1,
tenancy: 4,
tenancylength: 2,
hhmemb: 4,
age1_known: 0,
age1: 35,
sex1: "F",
ethnic_group: 0,
ethnic: 2,
nationality_all: 36,
ecstat1: 0,
details_known_2: 0,
relat2: "P",
age2_known: 0,
age2: 32,
sex2: "M",
ecstat2: 6,
details_known_3: 1,
details_known_4: 0,
relat4: "R",
age4_known: 1,
sex4: "R",
ecstat4: 10,
armedforces: 1,
leftreg: 4,
reservist: 1,
preg_occ: 2,
housingneeds: 1,
housingneeds_type: 0,
housingneeds_a: 1,
housingneeds_b: 0,
housingneeds_c: 0,
housingneeds_f: 0,
housingneeds_g: 0,
housingneeds_h: 0,
housingneeds_other: 0,
illness: 1,
illness_type_1: 0,
illness_type_2: 1,
illness_type_3: 0,
illness_type_4: 0,
illness_type_5: 0,
illness_type_6: 0,
illness_type_7: 0,
illness_type_8: 0,
illness_type_9: 0,
illness_type_10: 0,
layear: 2,
waityear: 7,
reason: 4,
prevten: 6,
homeless: 1,
ppcodenk: 1,
ppostcode_full: "TN23 6LZ",
previous_la_known: 1,
prevloc: "E07000105",
reasonpref: 1,
rp_homeless: 0,
rp_insan_unsat: 1,
rp_medwel: 0,
rp_hardship: 0,
rp_dontknow: 0,
cbl: 0,
chr: 1,
cap: 0,
accessible_register: 0,
referral: 2,
net_income_known: 0,
incref: 0,
incfreq: 1,
earnings: 268,
hb: 6,
has_benefits: 1,
benefits: 1,
period: 2,
brent: 200,
scharge: 50,
pscharge: 40,
supcharg: 35,
tcharge: 325,
hbrentshortfall: 1,
tshortfall_known: 1,
tshortfall: 12,
)
end
context "when exporting with human readable labels" do
let(:export_type) { "labels" }
context "when the current user is a support user" do
let(:user) { create(:user, :support, organisation:, email: "s.port@jeemayle.com") }
it "exports the CSV with 2024 ordering and all values correct" do
expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_labels_24.csv")
values_to_delete = %w[id]
values_to_delete.each do |attribute|
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv).to eq expected_content
end
end
context "when the current user is not a support user" do
let(:user) { create(:user, :data_provider, organisation:, email: "choreographer@owtluk.com") }
it "exports the CSV with all values correct" do
expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_non_support_labels_24.csv")
values_to_delete = %w[id]
values_to_delete.each do |attribute|
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv).to eq expected_content
end
end
end
context "when exporting values as codes" do
let(:export_type) { "codes" }
context "when the current user is a support user" do
let(:user) { create(:user, :support, organisation:, email: "s.port@jeemayle.com") }
it "exports the CSV with all values correct" do
expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_codes_24.csv")
values_to_delete = %w[id]
values_to_delete.each do |attribute|
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv).to eq expected_content
end
end
context "when the current user is not a support user" do
let(:user) { create(:user, :data_provider, organisation:, email: "choreographer@owtluk.com") }
it "exports the CSV with all values correct" do
expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_non_support_codes_24.csv")
values_to_delete = %w[id]
values_to_delete.each do |attribute|
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv).to eq expected_content
end
end
end
end
end end
end end
end end

47
spec/services/filter_manager_spec.rb

@ -94,4 +94,51 @@ describe FilterManager do
expect(described_class.filter_schemes(Scheme.all, nil, {}, nil, nil)).to eq(alphabetical_order_schemes) expect(described_class.filter_schemes(Scheme.all, nil, {}, nil, nil)).to eq(alphabetical_order_schemes)
end end
end end
describe "filter_users" do
let(:data_provider_user) { FactoryBot.create(:user, role: "data_provider") }
let(:data_coordinator_user) { FactoryBot.create(:user, role: "data_coordinator") }
let(:support_user) { FactoryBot.create(:user, role: "support") }
let(:key_contact_user) { FactoryBot.create(:user, is_key_contact: true) }
let(:dpo_user) { FactoryBot.create(:user, is_dpo: true) }
let(:key_contact_dpo_user) { FactoryBot.create(:user, is_key_contact: true, is_dpo: true) }
context "when filtering by role" do
it "returns users with the role" do
filter = { "role" => %w[data_provider] }
result = described_class.filter_users(User.all, nil, filter, nil)
expect(result).to include(data_provider_user)
expect(result).not_to include(data_coordinator_user)
expect(result).not_to include(support_user)
end
it "returns users with multiple roles selected" do
filter = { "role" => %w[data_provider data_coordinator] }
result = described_class.filter_users(User.all, nil, filter, nil)
expect(result).to include(data_provider_user)
expect(result).to include(data_coordinator_user)
expect(result).not_to include(support_user)
end
end
context "when filtering by additional responsibilities" do
it "returns users with the additional responsibilities" do
filter = { "additional_responsibilities" => %w[data_protection_officer] }
result = described_class.filter_users(User.all, nil, filter, nil)
expect(result).to include(dpo_user)
expect(result).to include(key_contact_dpo_user)
expect(result).not_to include(key_contact_user)
expect(result).not_to include(support_user)
end
it "returns users with multiple additional responsibilities selected" do
filter = { "additional_responsibilities" => %w[data_protection_officer key_contact] }
result = described_class.filter_users(User.all, nil, filter, nil)
expect(result).to include(dpo_user)
expect(result).to include(key_contact_dpo_user)
expect(result).to include(key_contact_user)
expect(result).not_to include(support_user)
end
end
end
end end

Loading…
Cancel
Save