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 4 days 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:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
@ -59,7 +58,7 @@ jobs:
uses: actions/setup-node@v4
with:
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
- name: Create local secret
@ -102,7 +101,6 @@ jobs:
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
@ -122,7 +120,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
node-version: 24
- name: Create database
run: |
@ -160,7 +158,6 @@ jobs:
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
@ -180,7 +177,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
node-version: 24
- name: Create database
run: |
@ -218,7 +215,6 @@ jobs:
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
@ -239,7 +235,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
node-version: 24
- name: Create local secret
run: |
@ -281,7 +277,6 @@ jobs:
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
@ -302,7 +297,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
node-version: 24
- name: Create local secret
run: |
@ -344,7 +339,6 @@ jobs:
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
@ -365,7 +359,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
node-version: 24
- name: Create database
run: |
@ -396,7 +390,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
node-version: 24
- name: Install packages and symlink local dependencies
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
# 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
RUN apk add --update --no-cache tzdata && \
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
# yarn: node package manager
# 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
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
# 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 \
&& tar -xvzf geckodriver-v0.31.0-linux64.tar.gz \
&& rm geckodriver-v0.31.0-linux64.tar.gz \
&& chmod +x geckodriver \
&& 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
@ -61,7 +64,7 @@ RUN bundle install --jobs=4 --no-binstubs --no-cache
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
@ -75,4 +78,4 @@ RUN chown -R nonroot performance_test
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"
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'
gem "rails", "~> 7.2.2"

2
Gemfile.lock

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

38
app/components/data_protection_confirmation_banner_component.rb

@ -12,16 +12,16 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
def display_banner?
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?
!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
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."
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."
elsif user.is_dpo?
"Your organisation must accept the Data Sharing Agreement before you can create any logs."
@ -31,7 +31,7 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
end
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(
link_text,
link_href,
@ -51,9 +51,9 @@ private
end
def link_text
if dpo_required?
if show_no_dpo_message?
"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"
else
"Read the Data Sharing Agreement"
@ -61,24 +61,32 @@ private
end
def link_href
if dpo_required?
if show_no_dpo_message?
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)
else
data_sharing_agreement_organisation_path(org_or_user_org)
end
end
def dpo_required?
org_or_user_org.data_protection_officers.empty?
def show_no_dpo_message?
# 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
def org_or_user_org
organisation.presence || user.organisation
def dsa_signed?
org_or_user_org.data_protection_confirmed?
end
def show_no_stock_owner_message?
!org_or_user_org.holds_own_stock? && dsa_signed?
end
def org_without_dpo?
org_or_user_org.data_protection_officers.empty?
def org_or_user_org
organisation.presence || user.organisation
end
end

2
app/frontend/controllers/numeric_question_controller.js

@ -11,7 +11,7 @@ export default class extends Controller {
calculateFields () {
const affectedField = this.element.dataset.target
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 elementToUpdate = document.getElementById(affectedField)
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)
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)
Time.zone.local(collection_start_year_for_date(date) + 1, 3, 31).end_of_day
end

18
app/helpers/filters_helper.rb

@ -52,6 +52,22 @@ module FiltersHelper
}.freeze
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
{
"incomplete" => "Incomplete",
@ -306,7 +322,7 @@ private
def filters_count(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?)
elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search uploading_organisation].include?(category)
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)
super(id, hsh, subsection)
@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 = {
"translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text",
"arguments" => [],
@ -11,7 +12,14 @@ class Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck < ::Form::
"translation" => "forms.#{form.start_date.year}.#{@copy_key}.informative_text",
"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
def questions

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

@ -3,8 +3,8 @@ class Form::Lettings::Pages::PropertyLocalAuthority < ::Form::Page
super
@id = "property_local_authority"
@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_2024_or_later?" => true, "address_search_given?" => true },
{ "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_2025_or_later?" => true },
]
end

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

@ -9,7 +9,7 @@ class Form::Lettings::Questions::Builtype < ::Form::Question
ANSWER_OPTIONS = {
"2" => { "value" => "Converted from previous residential or non-residential property" },
"1" => { "value" => "Purpose built" },
"1" => { "value" => "Purpose-built" },
}.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)
answer_options.select { |k, _v| scheme_location_ids.include?(k.to_i) }
.sort_by { |_, v|
name = v["hint"].match(/[a-zA-Z].*/).to_s
number = v["hint"].match(/\d+/).to_s.to_i
[name, number]
}.to_h
.sort_by { |_, v| v["value"] }
.to_h
end
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::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::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::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?),

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

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

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

@ -19,13 +19,18 @@ class Form::Sales::Questions::BuyerStillServing < ::Form::Question
else
{
"4" => { "value" => "Yes" },
"5" => { "value" => "No" },
"6" => { "value" => "Buyer prefers not to say" },
"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 },
"7" => { "value" => "Don’t know" },
"9" => { "value" => "Don’t know" },
"10" => { "value" => "No" },
}.freeze
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
end

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

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

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

@ -4,7 +4,7 @@ class Form::Sales::Questions::PurchasePrice < ::Form::Question
@id = "value"
@type = "numeric"
@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
@prefix = "£"
@ownership_sch = ownershipsch

21
app/models/lettings_log.rb

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

23
app/models/user.rb

@ -81,6 +81,29 @@ class User < ApplicationRecord
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 :deactivated, -> { where(active: false) }
scope :activated, -> { where(active: true) }

9
app/models/validations/financial_validations.rb

@ -175,6 +175,15 @@ module Validations::FinancialValidations
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
def validate_charges(record)

2
app/services/feature_toggle.rb

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

8
app/services/filter_manager.rb

@ -130,6 +130,14 @@ class FilterManager
new_filters["status"] = params["status"]
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")
current_user.scheme_filters(specific_org:).each do |filter|
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",
locals: {
f:,
options: user_status_filters,
label: "Status",
category: "status",
size: "s",
} %>
f:,
options: user_status_filters,
label: "Status",
category: "status",
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? %>
<%= f.hidden_field :search, value: request.params["search"] %>

8
aws-devcontainer/.devcontainer/Dockerfile

@ -1,7 +1,13 @@
FROM homebrew/brew
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_FILE_DIR=./vault

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

@ -25,10 +25,10 @@ en:
hb:
page_header: ""
check_answer_label: "Housing related benefits received"
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."
question_text: "Is the household likely to be receiving any of these housing related benefits?"
check_answer_label: "Housing-related benefits received"
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."
question_text: "Is the household likely to be receiving any of these housing-related benefits?"
benefits:
page_header: ""
@ -112,7 +112,7 @@ en:
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"
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:
page_header: ""

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

@ -46,16 +46,16 @@ en:
housing_benefits:
joint_purchase:
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: ""
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:
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: ""
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:
joint_purchase:

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

@ -25,10 +25,10 @@ en:
hb:
page_header: ""
check_answer_label: "Housing related benefits received"
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."
question_text: "Is the household likely to be receiving any of these housing related benefits?"
check_answer_label: "Housing-related benefits received"
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."
question_text: "Is the household likely to be receiving any of these housing-related benefits?"
benefits:
page_header: ""
@ -112,7 +112,7 @@ en:
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"
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:
page_header: ""

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

@ -46,16 +46,16 @@ en:
housing_benefits:
joint_purchase:
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: ""
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:
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: ""
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:
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."
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."
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:
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."
@ -87,3 +88,5 @@ en:
needstype:
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."
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)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
base64 (0.3.0)
benchmark (0.5.0)
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
```bash
rbenv install 3.4.4
rbenv global 3.4.4
rbenv install 3.4.9
rbenv global 3.4.9
source ~/.bashrc
gem install bundler
```
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):
```bash
nvm install 20
nvm use 20
nvm install 24
nvm use 24
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",
"private": true,
"engines": {
"node": "^20.0.0"
"node": "^24.0.0"
},
"dependencies": {
"@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
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.")
context "when org does not have a signed data sharing agreement" do
let(:organisation) { create(:organisation, :without_dpc) }
let(:user) { create(:user, organisation:, with_dsa: false) }
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 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
@ -81,7 +93,7 @@ RSpec.describe DataProtectionConfirmationBannerComponent, type: :component do
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
expect(component.display_banner?).to be(false)
expect(render.content).to be_empty
@ -121,88 +133,5 @@ RSpec.describe DataProtectionConfirmationBannerComponent, type: :component do
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

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)
expect(find("#lettings-log-tcharge-field").value).to eq("550.00")
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

20
spec/features/user_spec.rb

@ -282,6 +282,12 @@ RSpec.describe "User Features" do
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
@ -619,6 +625,20 @@ RSpec.describe "User Features" do
expect(page).to have_button("Resend invite link")
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
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
let(:log) { build(:lettings_log) }
context "with form before 2024" do
before do
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
before do
allow(form).to receive(:start_year_2025_or_later?).and_return(true)
end
context "with form after 2024" do
before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
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 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 it's a supported housing log" do
log.needstype = 2
log.is_la_inferred = false
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 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 address search is not given" do
log.needstype = 1
log.is_la_inferred = false
log.address_line1_input = nil
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
log.needstype = 2
log.is_la_inferred = false
expect(page).not_to be_routed_to(log, nil)
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
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, 6), name: "1 Abe Road")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 7), name: "1 Lake Lane")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 8), name: "3 Abe Road")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 9), name: "2 Lake Lane")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 10), name: "Smith Avenue")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 11), name: "Abacus Road")
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12), name: "Hawthorne 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", postcode: "AA1 2AA")
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", postcode: "AA1 4AA")
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", postcode: "AA1 6AA")
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", postcode: "AA1 8AA")
lettings_log.update!(scheme:)
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([
"Abacus Road",
"1 Abe Road",
"2 Abe Road",
"3 Abe Road",
"Hawthorne Road",
"1 Abe Road",
"1 Lake Lane",
"3 Abe Road",
"2 Lake Lane",
"Smith Avenue",
"Abacus Road",
"Hawthorne Road",
])
end
end

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

@ -1,12 +1,15 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::PropertyBuildingType, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { 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(:saledate) { current_collection_start_date }
it "has correct subsection" do
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
end
context "with form year 2024" do
let(:form) { Form.new(nil, 2024, [], "sales") }
let(:saledate) { Time.zone.local(2024, 4, 1) }
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:) }
context "with a staircasing log" do
let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
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
it "is not routed to" do
expect(page.routed_to?(log, nil)).to be false
end
end
context "with form year 2025" do
let(:form) { Form.new(nil, 2025, [], "sales") }
let(:saledate) { Time.zone.local(2025, 4, 1) }
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:) }
context "with a non-staircasing log" do
let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
let(:log) { build(:sales_log, staircase: nil, saledate:) }
it "is routed to" do
expect(page.routed_to?(log, nil)).to be true
end
it "is routed to" do
expect(page.routed_to?(log, nil)).to be true
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)
end
describe "has correct questions" do
context "when 2023" do
let(:start_date) { Time.utc(2023, 2, 8) }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(
%w[
la
],
)
end
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[la])
end
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
let(:log) { build(:sales_log) }
context "with form before 2024" do
before do
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
before do
allow(form).to receive(:start_year_2025_or_later?).and_return(true)
end
context "with form after 2024" do
before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
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 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 address search is not given" do
log.is_la_inferred = false
log.address_line1_input = nil
log.postcode_full_input = "A11AA"
expect(page).not_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

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

@ -1,12 +1,15 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::PropertyWheelchairAccessible, type: :model do
include CollectionTimeHelper
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { 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(:saledate) { current_collection_start_date }
it "has correct subsection" do
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
end
context "with form year 2024" do
let(:form) { Form.new(nil, 2024, [], "sales") }
let(:saledate) { Time.zone.local(2024, 4, 1) }
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:) }
context "with a staircasing log" do
let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
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
it "is not routed to" do
expect(page.routed_to?(log, nil)).to be false
end
end
context "with form year 2025" do
let(:form) { Form.new(nil, 2025, [], "sales") }
let(:saledate) { Time.zone.local(2025, 4, 1) }
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:) }
context "with a non-staircasing log" do
let(:form) { Form.new(nil, current_collection_start_year, [], "sales") }
let(:log) { build(:sales_log, :shared_ownership_setup_complete, staircase: 2, saledate:) }
it "is routed to" do
expect(page.routed_to?(log, nil)).to be true
end
it "is routed to" do
expect(page.routed_to?(log, nil)).to be true
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
expect(question.answer_options).to eq({
"4" => { "value" => "Yes" },
"5" => { "value" => "No" },
"6" => { "value" => "Buyer prefers not to say" },
"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 },
"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
@ -52,5 +63,15 @@ RSpec.describe Form::Sales::Questions::BuyerStillServing, type: :model do
"9" => { "value" => "Don’t know" },
})
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

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
expect(question.answer_options).to eq({
"1" => { "value" => "Purpose built" },
"1" => { "value" => "Purpose-built" },
"2" => { "value" => "Converted from previous residential or non-residential property" },
})
end

39
spec/models/lettings_log_derived_fields_spec.rb

@ -111,45 +111,6 @@ RSpec.describe LettingsLog, type: :model do
end
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
let(:startdate) { collection_start_date_for_year(2025) }

104
spec/models/validations/date_validations_spec.rb

@ -1,6 +1,8 @@
require "rails_helper"
RSpec.describe Validations::DateValidations do
include CollectionTimeHelper
subject(:date_validator) { validator_class.new }
let(:validator_class) { Class.new { include Validations::DateValidations } }
@ -54,44 +56,22 @@ RSpec.describe Validations::DateValidations do
expect(record.errors["mrcdate"]).to be_empty
end
context "with 2024 logs or earlier" do
it "cannot be more than 10 years before the tenancy start date" do
record.startdate = Time.zone.local(2024, 4, 1)
record.mrcdate = Time.zone.local(2014, 1, 31)
date_validator.validate_property_major_repairs(record)
expect(record.errors["mrcdate"])
.to include(match I18n.t("validations.lettings.date.mrcdate.ten_years_before_tenancy_start"))
expect(record.errors["startdate"])
.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
it "cannot be more than 20 years before the tenancy start date" do
record.startdate = current_collection_start_date
record.mrcdate = current_collection_start_date - 20.years - 1.day
date_validator.validate_property_major_repairs(record)
expect(record.errors["mrcdate"])
.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
context "with 2025 logs or later" do
it "cannot be more than 20 years before the tenancy start date" do
record.startdate = Time.zone.local(2026, 2, 1)
record.mrcdate = Time.zone.local(2006, 1, 31)
date_validator.validate_property_major_repairs(record)
expect(record.errors["mrcdate"])
.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
it "can be within 20 years of the tenancy start date" do
record.startdate = current_collection_start_date
record.mrcdate = current_collection_start_date - 20.years
date_validator.validate_property_major_repairs(record)
expect(record.errors["mrcdate"]).to be_empty
expect(record.errors["startdate"]).to be_empty
end
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
end
context "with 2024 logs or earlier" do
it "cannot be more than 10 years before the tenancy start date" do
record.startdate = Time.zone.local(2024, 4, 1)
record.voiddate = Time.zone.local(2014, 1, 31)
date_validator.validate_property_void_date(record)
expect(record.errors["voiddate"])
.to include(match I18n.t("validations.lettings.date.void_date.ten_years_before_tenancy_start"))
expect(record.errors["startdate"])
.to include(match I18n.t("validations.lettings.date.startdate.ten_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
it "cannot be more than 20 years before the tenancy start date" do
record.startdate = current_collection_start_date
record.voiddate = current_collection_start_date - 20.years - 1.day
date_validator.validate_property_void_date(record)
date_validator.validate_startdate(record)
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
context "with 2025 logs or later" do
it "cannot be more than 20 years before the tenancy start date" do
record.startdate = Time.zone.local(2026, 2, 1)
record.voiddate = Time.zone.local(2006, 1, 31)
date_validator.validate_property_void_date(record)
date_validator.validate_startdate(record)
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
it "can be within 20 years of the tenancy start date" do
record.startdate = current_collection_start_date
record.voiddate = current_collection_start_date - 20.years
date_validator.validate_property_void_date(record)
expect(record.errors["voiddate"]).to be_empty
expect(record.errors["startdate"]).to be_empty
end
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(: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
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
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

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
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
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
record.income1 = 13_399

2
spec/requests/check_errors_controller_spec.rb

@ -84,7 +84,7 @@ RSpec.describe CheckErrorsController, type: :request do
end
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_button("Clear all")
end

6
spec/requests/collection_resources_controller_spec.rb

@ -734,7 +734,7 @@ RSpec.describe CollectionResourcesController, type: :request do
end
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
it "redirects to the sign in page" do
@ -773,7 +773,7 @@ RSpec.describe CollectionResourcesController, type: :request do
let(:user) { create(:user, :support) }
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)
sign_in user
end
@ -786,7 +786,7 @@ RSpec.describe CollectionResourcesController, type: :request do
it "displays update collection resources page content" do
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("This file will be available for all users to download.")
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"
RSpec.describe Csv::LettingsLogCsvService do
include CollectionTimeHelper
subject(:task) { Rake::Task["data_import:add_variable_definitions"] }
before do
@ -16,7 +18,7 @@ RSpec.describe Csv::LettingsLogCsvService do
let(:user) { create(:user, :support, email: "s.port@jeemayle.com") }
let(:service) { described_class.new(user:, export_type:, year:) }
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(:logs) { [log] }
let(:definition_line) { csv.first }
@ -582,199 +584,6 @@ RSpec.describe Csv::LettingsLogCsvService do
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

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)
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

Loading…
Cancel
Save