Browse Source

Merge branch 'main' into CLDC-NONE-document-best-practices-for-time-travel

pull/3330/head
Nat Dean-Lewis 2 weeks ago
parent
commit
2c60ba0e3a
  1. 20
      .github/workflows/run_tests.yml
  2. 2
      .nvmrc
  3. 2
      .ruby-version
  4. 15
      Dockerfile
  5. 10
      Gemfile
  6. 105
      Gemfile.lock
  7. 4
      app/components/bulk_upload_error_row_component.html.erb
  8. 9
      app/components/bulk_upload_error_row_component.rb
  9. 4
      app/components/bulk_upload_error_summary_table_component.html.erb
  10. 3
      app/components/bulk_upload_error_summary_table_component.rb
  11. 16
      app/components/bulk_upload_summary_component.rb
  12. 6
      app/components/check_answers_summary_list_card_component.html.erb
  13. 11
      app/components/check_answers_summary_list_card_component.rb
  14. 18
      app/components/create_log_actions_component.html.erb
  15. 19
      app/components/create_log_actions_component.rb
  16. 2
      app/components/data_protection_confirmation_banner_component.html.erb
  17. 5
      app/components/data_protection_confirmation_banner_component.rb
  18. 2
      app/components/document_list_component.html.erb
  19. 2
      app/components/document_list_component.rb
  20. 2
      app/components/lettings_log_summary_component.html.erb
  21. 2
      app/components/lettings_log_summary_component.rb
  22. 2
      app/components/missing_stock_owners_banner_component.html.erb
  23. 9
      app/components/missing_stock_owners_banner_component.rb
  24. 2
      app/components/primary_navigation_component.html.erb
  25. 2
      app/components/primary_navigation_component.rb
  26. 2
      app/components/sales_log_summary_component.html.erb
  27. 2
      app/components/sales_log_summary_component.rb
  28. 4
      app/components/search_component.html.erb
  29. 2
      app/components/search_component.rb
  30. 2
      app/components/search_result_caption_component.rb
  31. 4
      app/components/sub_navigation_component.html.erb
  32. 2
      app/components/sub_navigation_component.rb
  33. 6
      app/controllers/auth/confirmations_controller.rb
  34. 1
      app/controllers/auth/passwords_controller.rb
  35. 10
      app/controllers/users_controller.rb
  36. 2
      app/frontend/controllers/numeric_question_controller.js
  37. 2
      app/frontend/styles/_filter.scss
  38. 11
      app/frontend/styles/_header.scss
  39. 2
      app/frontend/styles/_related-navigation.scss
  40. 2
      app/frontend/styles/_tag.scss
  41. 2
      app/frontend/styles/_testing-tools.scss
  42. 12
      app/frontend/styles/application.scss
  43. 13
      app/helpers/application_helper.rb
  44. 2
      app/models/derived_variables/lettings_log_variables.rb
  45. 25
      app/models/form/lettings/pages/net_income_value_check.rb
  46. 12
      app/models/form/lettings/pages/no_household_member_likely_to_be_pregnant_check.rb
  47. 9
      app/models/form/lettings/pages/person_known.rb
  48. 4
      app/models/form/lettings/pages/property_local_authority.rb
  49. 2
      app/models/form/lettings/questions/builtype.rb
  50. 2
      app/models/form/lettings/questions/hhmemb.rb
  51. 12
      app/models/form/lettings/subsections/household_characteristics.rb
  52. 2
      app/models/form/lettings/subsections/income_and_benefits.rb
  53. 4
      app/models/form/sales/pages/property_local_authority.rb
  54. 1
      app/models/form/sales/questions/building_height_class.rb
  55. 11
      app/models/form/sales/questions/buyer_still_serving.rb
  56. 1
      app/models/form/sales/questions/management_fee.rb
  57. 1
      app/models/form/sales/questions/monthly_rent_before_staircasing.rb
  58. 1
      app/models/form/sales/questions/mortgage_amount.rb
  59. 2
      app/models/form/sales/questions/property_building_type.rb
  60. 3
      app/models/form/sales/questions/purchase_price.rb
  61. 1
      app/models/form/sales/questions/value.rb
  62. 6
      app/models/forms/bulk_upload_resume/confirm.rb
  63. 6
      app/models/forms/bulk_upload_resume/fix_choice.rb
  64. 24
      app/models/lettings_log.rb
  65. 8
      app/models/log.rb
  66. 1
      app/models/sales_log.rb
  67. 19
      app/models/user.rb
  68. 13
      app/models/validations/financial_validations.rb
  69. 6
      app/models/validations/sales/sale_information_validations.rb
  70. 35
      app/models/validations/soft_validations.rb
  71. 11
      app/services/bulk_upload/lettings/year2025/row_parser.rb
  72. 11
      app/services/bulk_upload/lettings/year2026/row_parser.rb
  73. 30
      app/services/bulk_upload/sales/year2025/row_parser.rb
  74. 29
      app/services/bulk_upload/sales/year2026/row_parser.rb
  75. 6
      app/services/feature_toggle.rb
  76. 4
      app/views/bulk_upload_lettings_results/show.html.erb
  77. 8
      app/views/bulk_upload_lettings_results/summary.html.erb
  78. 2
      app/views/bulk_upload_lettings_resume/confirm.html.erb
  79. 2
      app/views/bulk_upload_lettings_resume/fix_choice.html.erb
  80. 4
      app/views/bulk_upload_sales_results/show.html.erb
  81. 8
      app/views/bulk_upload_sales_results/summary.html.erb
  82. 2
      app/views/bulk_upload_sales_resume/confirm.html.erb
  83. 2
      app/views/bulk_upload_sales_resume/fix_choice.html.erb
  84. 5
      app/views/form/guidance/_building_height_class.html.erb
  85. 6
      app/views/form/headers/_person_2_known_page.erb
  86. 6
      app/views/form/headers/_person_3_known_page.erb
  87. 6
      app/views/form/headers/_person_4_known_page.erb
  88. 6
      app/views/form/headers/_person_5_known_page.erb
  89. 6
      app/views/form/headers/_person_6_known_page.erb
  90. 30
      app/views/layouts/application.html.erb
  91. 18
      app/views/layouts/rails_admin/_navigation.html.erb
  92. 4
      app/views/users/_user_list.html.erb
  93. 2
      app/views/users/new.html.erb
  94. 8
      aws-devcontainer/.devcontainer/Dockerfile
  95. 4
      config/locales/en.yml
  96. 2
      config/locales/forms/2024/sales/sale_information.en.yml
  97. 2
      config/locales/forms/2025/lettings/household_characteristics.en.yml
  98. 2
      config/locales/forms/2025/lettings/household_situation.en.yml
  99. 2
      config/locales/forms/2025/sales/sale_information.en.yml
  100. 2
      config/locales/forms/2026/lettings/household_characteristics.en.yml
  101. Some files were not shown because too many files have changed in this diff Show More

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"]

10
Gemfile

@ -3,14 +3,14 @@
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"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use Puma as the app server
gem "puma", "~> 6.4"
gem "puma", "~> 7.2.1"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]
@ -18,7 +18,7 @@ gem "jsbundling-rails"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", ">= 1.4.4", require: false
# GOV UK frontend components
gem "govuk-components", "~> 5.7"
gem "govuk-components", "~> 6.2"
# GOV UK component form builder DSL
gem "govuk_design_system_formbuilder", "~> 5.7"
# Convert Markdown into GOV.UK frontend-styled HTML
@ -40,7 +40,7 @@ gem "devise_two_factor_authentication"
gem "uk_postcode"
# Get rich data from postcode lookups. Wraps postcodes.io
# Use Ruby objects to build reusable markup. A React inspired evolution of the presenter pattern
gem "view_component", "~> 3.9"
gem "view_component", "~> 4.9"
# Use the AWS S3 SDK as storage mechanism
gem "aws-sdk-s3"
# Track changes to models for auditing or versioning.
@ -67,7 +67,7 @@ gem "faker"
gem "method_source", "~> 1.1"
gem "rails_admin", "~> 3.1"
gem "ruby-openai"
gem "sidekiq"
gem "sidekiq", "~> 7.2.4"
gem "sidekiq-cron"
gem "unread"

105
Gemfile.lock

@ -78,8 +78,8 @@ GEM
minitest (>= 5.1, < 6)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
auto_strip_attributes (2.6.0)
activerecord (>= 4.0)
@ -123,7 +123,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (4.0.1)
bigdecimal (4.1.2)
bindex (0.8.1)
bootsnap (1.18.3)
msgpack (~> 1.2)
@ -155,18 +155,21 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.3.6)
connection_pool (2.5.3)
connection_pool (2.5.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
cronex (0.15.0)
tzinfo
unicode (>= 0.4.4.5)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
csv (3.3.2)
date (3.5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (5.0.3)
devise (5.0.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 7.0)
@ -187,7 +190,7 @@ GEM
drb (2.2.3)
dumb_delegator (1.0.0)
encryptor (3.0.0)
erb (6.0.2)
erb (6.0.4)
erb_lint (0.9.0)
activesupport
better_html (>= 2.0.1)
@ -196,7 +199,7 @@ GEM
rubocop (>= 1)
smart_properties
erubi (1.13.1)
et-orbi (1.2.11)
et-orbi (1.4.0)
tzinfo
event_stream_parser (1.0.0)
excon (0.111.0)
@ -207,24 +210,24 @@ GEM
railties (>= 5.0.0)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
faraday (2.14.1)
faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.1.0)
net-http
faraday-net_http (3.4.3)
net-http (~> 0.5)
ffi (1.16.3)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
fugit (1.12.2)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.2.1)
globalid (1.3.0)
activesupport (>= 6.1)
govuk-components (5.7.0)
govuk-components (6.2.0)
html-attributes-utils (~> 1.0.0, >= 1.0.0)
pagy (>= 6, < 10)
view_component (>= 3.9, < 3.17)
view_component (>= 4.9, < 4.10)
govuk_design_system_formbuilder (5.7.1)
actionview (>= 6.1)
activemodel (>= 6.1)
@ -241,7 +244,7 @@ GEM
ice_nine (0.11.2)
iniparse (1.5.0)
io-console (0.8.2)
irb (1.17.0)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
@ -249,10 +252,10 @@ GEM
jmespath (1.6.2)
jsbundling-rails (1.3.0)
railties (>= 6.0.0)
json (2.19.2)
json (2.19.8)
json-schema (4.1.1)
addressable (>= 2.8)
jwt (2.8.0)
jwt (3.2.0)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
@ -290,9 +293,9 @@ GEM
msgpack (1.7.2)
multipart-post (2.4.1)
nested_form (0.3.2)
net-http (0.4.1)
uri
net-imap (0.5.7)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.4)
date
net-protocol
net-pop (0.1.2)
@ -301,23 +304,23 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.19.1-arm64-darwin)
nio4r (2.7.5)
nokogiri (1.19.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-darwin)
nokogiri (1.19.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu)
nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl)
nokogiri (1.19.3-x86_64-linux-musl)
racc (~> 1.4)
notifications-ruby-client (6.0.0)
jwt (>= 1.5, < 3)
notifications-ruby-client (6.4.0)
jwt (>= 1.5, < 4)
orm_adapter (0.5.0)
overcommit (0.63.0)
childprocess (>= 0.6.3, < 6)
iniparse (~> 1.4)
rexml (~> 3.2)
pagy (9.3.2)
pagy (9.4.0)
paper_trail (15.2.0)
activerecord (>= 6.1)
request_store (~> 1.4)
@ -350,19 +353,19 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (5.0.4)
puma (6.5.0)
public_suffix (7.0.5)
puma (7.2.1)
nio4r (~> 2.0)
pundit (2.3.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.20)
rack (3.1.21)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-mini-profiler (3.3.1)
rack (>= 1.2.0)
rack-session (2.1.1)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@ -408,7 +411,7 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rake (13.4.2)
randexp (0.1.7)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
@ -419,7 +422,7 @@ GEM
tsort
redcarpet (3.6.0)
redis (4.8.1)
redis-client (0.22.1)
redis-client (0.29.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
@ -513,10 +516,11 @@ GEM
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.19.0)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
sidekiq-cron (2.4.0)
cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1)
sidekiq (>= 6)
sidekiq (>= 6.5.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@ -530,7 +534,7 @@ GEM
thor (1.4.0)
thread_safe (0.3.6)
timecop (0.9.8)
timeout (0.4.3)
timeout (0.6.1)
tsort (0.2.0)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
@ -538,17 +542,18 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uk_postcode (2.1.8)
unicode (0.4.4.5)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
unread (0.14.0)
activerecord (>= 6.1)
uri (1.0.4)
uri (1.1.1)
useragent (0.16.11)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
view_component (4.9.0)
actionview (>= 7.1.0)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)
virtus (2.0.0)
axiom-types (~> 0.1)
coercible (~> 1.0)
@ -571,7 +576,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.5)
zeitwerk (2.8.2)
PLATFORMS
arm64-darwin
@ -599,7 +604,7 @@ DEPENDENCIES
factory_bot_rails
faker
faraday (>= 2.14.1)
govuk-components (~> 5.7)
govuk-components (~> 6.2)
govuk_design_system_formbuilder (~> 5.7)
govuk_markdown
jsbundling-rails
@ -616,7 +621,7 @@ DEPENDENCIES
possessive
propshaft
pry-byebug
puma (~> 6.4)
puma (~> 7.2.1)
pundit
rack (~> 3.1.20)
rack-attack
@ -634,7 +639,7 @@ DEPENDENCIES
selenium-webdriver
sentry-rails
sentry-ruby
sidekiq
sidekiq (~> 7.2.4)
sidekiq-cron
simplecov
stimulus-rails
@ -643,12 +648,12 @@ DEPENDENCIES
tzinfo-data
uk_postcode
unread
view_component (~> 3.9)
view_component (~> 4.9)
web-console (>= 4.1.0)
webmock
RUBY VERSION
ruby 3.4.4p0
ruby 3.4.9p82
BUNDLED WITH
2.6.4

4
app/components/bulk_upload_error_row_component.html.erb

@ -13,7 +13,7 @@
<% if critical_errors.any? %>
<h2 class="govuk-heading-m">Critical errors</h2>
<p class="govuk-body">These errors must be fixed to complete your logs.</p>
<%= govuk_table(html_attributes: { class: potential_errors.any? ? "" : "no-bottom-border" }) do |table| %>
<%= helpers.govuk_table(html_attributes: { class: potential_errors.any? ? "" : "no-bottom-border" }) do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(header: true, text: "Cell") %>
@ -39,7 +39,7 @@
<% if potential_errors.any? %>
<h2 class="govuk-heading-m">Confirmation needed</h2>
<p class="govuk-body">Potential data discrepancies exist in the following cells.<br><br>Please resolve all critical errors and review the cells with data discrepancies before re-uploading the file. Bulk confirmation of potential discrepancies is accessible only after all critical errors have been resolved.</p>
<%= govuk_table(html_attributes: { class: "no-bottom-border" }) do |table| %>
<%= helpers.govuk_table(html_attributes: { class: "no-bottom-border" }) do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(header: true, text: "Cell") %>

9
app/components/bulk_upload_error_row_component.rb

@ -2,9 +2,8 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
attr_reader :bulk_upload_errors
def initialize(bulk_upload_errors:)
super()
@bulk_upload_errors = bulk_upload_errors
super
end
def row
@ -18,7 +17,7 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
def tenant_code_html
return if tenant_code.blank?
content_tag :span, class: "govuk-!-margin-left-3" do
helpers.content_tag :span, class: "govuk-!-margin-left-3" do
"Tenant code: #{tenant_code}"
end
end
@ -30,7 +29,7 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
def purchaser_code_html
return if purchaser_code.blank?
content_tag :span, class: "govuk-!-margin-left-3" do
helpers.content_tag :span, class: "govuk-!-margin-left-3" do
"Purchaser code: #{purchaser_code}"
end
end
@ -42,7 +41,7 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
def property_ref_html
return if property_ref.blank?
content_tag :span, class: "govuk-!-margin-left-3" do
helpers.content_tag :span, class: "govuk-!-margin-left-3" do
"Property reference: #{property_ref}"
end
end

4
app/components/bulk_upload_error_summary_table_component.html.erb

@ -3,7 +3,7 @@
</p>
<% sorted_errors.each do |error| %>
<%= govuk_table do |table| %>
<%= helpers.govuk_table do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(text: question_for_field(error[0][1].to_sym), header: true) %>
@ -13,7 +13,7 @@
<%= table.with_body do |body| %>
<% body.with_row do |row| %>
<% row.with_cell(text: error[0][2].html_safe) %>
<% row.with_cell(text: pluralize(error[1], "error"), numeric: true) %>
<% row.with_cell(text: helpers.pluralize(error[1], "error"), numeric: true) %>
<% end %>
<% end %>
<% end %>

3
app/components/bulk_upload_error_summary_table_component.rb

@ -6,9 +6,8 @@ class BulkUploadErrorSummaryTableComponent < ViewComponent::Base
delegate :question_for_field, to: :row_parser_class
def initialize(bulk_upload:)
super()
@bulk_upload = bulk_upload
super
end
def sorted_errors

16
app/components/bulk_upload_summary_component.rb

@ -2,9 +2,9 @@ class BulkUploadSummaryComponent < ViewComponent::Base
attr_reader :bulk_upload
def initialize(bulk_upload:)
super()
@bulk_upload = bulk_upload
@bulk_upload_errors = bulk_upload.bulk_upload_errors
super
end
def upload_status
@ -27,9 +27,9 @@ class BulkUploadSummaryComponent < ViewComponent::Base
return if count.nil? || count <= 0
text = count > 1 ? (plural_text || singular_text.pluralize(count)) : singular_text
content_tag(:p, class: "govuk-!-font-size-16 govuk-!-margin-bottom-1") do
concat(content_tag(:strong, count))
concat(" #{text}")
helpers.content_tag(:p, class: "govuk-!-font-size-16 govuk-!-margin-bottom-1") do
helpers.concat(helpers.content_tag(:strong, count))
helpers.concat(" #{text}")
end
end
@ -44,11 +44,11 @@ class BulkUploadSummaryComponent < ViewComponent::Base
end
def download_lettings_file_link(bulk_upload)
govuk_link_to "Download file", download_lettings_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
helpers.govuk_link_to "Download file", download_lettings_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
end
def download_sales_file_link(bulk_upload)
govuk_link_to "Download file", download_sales_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
helpers.govuk_link_to "Download file", download_sales_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
end
def view_error_report_link(bulk_upload)
@ -61,12 +61,12 @@ class BulkUploadSummaryComponent < ViewComponent::Base
"bulk_upload_#{bulk_upload.log_type}_result_path"
end
govuk_link_to "View error report", send(path, bulk_upload), class: "govuk-link"
helpers.govuk_link_to "View error report", helpers.send(path, bulk_upload), class: "govuk-link"
end
def view_logs_link(bulk_upload)
return unless bulk_upload.status.to_s == "logs_uploaded_with_errors"
govuk_link_to "View logs with errors", send("#{bulk_upload.log_type}_logs_path", bulk_upload_id: [bulk_upload.id]), class: "govuk-link"
helpers.govuk_link_to "View logs with errors", helpers.send("#{bulk_upload.log_type}_logs_path", bulk_upload_id: [bulk_upload.id]), class: "govuk-link"
end
end

6
app/components/check_answers_summary_list_card_component.html.erb

@ -7,12 +7,12 @@
<% end %>
<div class="govuk-summary-card__content">
<%= govuk_summary_list do |summary_list| %>
<%= helpers.govuk_summary_list do |summary_list| %>
<% applicable_questions.each do |question| %>
<% summary_list.with_row do |row| %>
<% row.with_key { get_question_label(question) } %>
<% row.with_value do %>
<%= simple_format(
<%= helpers.simple_format(
get_answer_label(question),
wrapper_tag: "span",
class: "govuk-!-margin-right-4",
@ -21,7 +21,7 @@
<% extra_value = question.get_extra_check_answer_value(log) %>
<% if extra_value && question.answer_label(log).present? %>
<%= simple_format(
<%= helpers.simple_format(
extra_value,
wrapper_tag: "span",
class: "govuk-!-font-weight-regular app-!-colour-muted",

11
app/components/check_answers_summary_list_card_component.rb

@ -2,12 +2,11 @@ class CheckAnswersSummaryListCardComponent < ViewComponent::Base
attr_reader :questions, :log, :user
def initialize(questions:, log:, user:, correcting_hard_validation: false)
super()
@questions = questions
@log = log
@user = user
@correcting_hard_validation = correcting_hard_validation
super
end
def applicable_questions
@ -34,16 +33,16 @@ class CheckAnswersSummaryListCardComponent < ViewComponent::Base
def action_href(question, log)
referrer = question.displayed_as_answered?(log) ? "check_answers" : "check_answers_new_answer"
send("#{log.log_type}_#{question.page.id}_path", log, referrer:)
helpers.send("#{log.log_type}_#{question.page.id}_path", log, referrer:)
end
def correct_validation_action_href(question, log, _related_question_ids, correcting_hard_validation)
return action_href(question, log) unless correcting_hard_validation
if question.displayed_as_answered?(log)
send("#{log.log_type}_confirm_clear_answer_path", log, question_id: question.id)
helpers.send("#{log.log_type}_confirm_clear_answer_path", log, question_id: question.id)
else
send("#{log.log_type}_#{question.page.id}_path", log, referrer: "check_errors", related_question_ids: request.query_parameters["related_question_ids"], original_page_id: request.query_parameters["original_page_id"])
helpers.send("#{log.log_type}_#{question.page.id}_path", log, referrer: "check_errors", related_question_ids: request.query_parameters["related_question_ids"], original_page_id: request.query_parameters["original_page_id"])
end
end
@ -56,7 +55,7 @@ private
"govuk-link govuk-link--no-visited-state"
end
govuk_link_to question.check_answer_prompt, correct_validation_action_href(question, log, nil, @correcting_hard_validation), class: link_class
helpers.govuk_link_to question.check_answer_prompt, correct_validation_action_href(question, log, nil, @correcting_hard_validation), class: link_class
end
def number_of_buyers

18
app/components/create_log_actions_component.html.erb

@ -1,11 +1,11 @@
<div class="govuk-button-group app-filter-toggle <%= "govuk-!-margin-bottom-6" if display_actions? %>">
<% if display_actions? %>
<%= govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-3" %>
<%= helpers.govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-3" %>
<% unless user.support? %>
<%= govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %>
<%= helpers.govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %>
<% end %>
<% if user.support? %>
<%= govuk_button_link_to view_uploads_button_copy, view_uploads_button_href, secondary: true %>
<%= helpers.govuk_button_link_to view_uploads_button_copy, view_uploads_button_href, secondary: true %>
<% end %>
<% if FeatureToggle.create_test_logs_enabled? %>
@ -13,42 +13,42 @@
<span class="govuk-tag app-testing-tools__tag">Testing tools</span>
<span class="govuk-body govuk-body-s">These tools can only be seen and used in testing environments.</span>
<div>
<%= govuk_button_link_to create_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_test_log_href, class: "govuk-button" do %>
New <%= current_collection_year_label %> test log
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% if FeatureToggle.allow_future_form_use? %>
<%= govuk_button_link_to create_next_year_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_next_year_test_log_href, class: "govuk-button" do %>
New <%= next_collection_year_label %> test log
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% end %>
<%= govuk_button_link_to create_setup_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_setup_test_log_href, class: "govuk-button" do %>
New <%= current_collection_year_label %> test log (setup only)
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% if FeatureToggle.allow_future_form_use? %>
<%= govuk_button_link_to create_next_year_setup_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_next_year_setup_test_log_href, class: "govuk-button" do %>
New <%= next_collection_year_label %> test log (setup only)
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% end %>
<%= govuk_button_link_to create_test_bulk_upload_href(2025), class: "govuk-button govuk-button--secondary" do %>
<%= helpers.govuk_button_link_to create_test_bulk_upload_href(2025), class: "govuk-button govuk-button--secondary" do %>
25/26 BU test file
<svg class="govuk-button__start-icon bi bi-download" xmlns="http://www.w3.org/2000/svg" width="18" height="19" fill="currentColor" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />
</svg>
<% end %>
<%= govuk_button_link_to create_test_bulk_upload_href(2026), class: "govuk-button govuk-button--secondary" do %>
<%= helpers.govuk_button_link_to create_test_bulk_upload_href(2026), class: "govuk-button govuk-button--secondary" do %>
26/27 BU test file
<svg class="govuk-button__start-icon bi bi-download" xmlns="http://www.w3.org/2000/svg" width="18" height="19" fill="currentColor" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />

19
app/components/create_log_actions_component.rb

@ -5,11 +5,10 @@ class CreateLogActionsComponent < ViewComponent::Base
attr_reader :bulk_upload, :user, :log_type
def initialize(user:, log_type:, bulk_upload: nil)
super()
@bulk_upload = bulk_upload
@user = user
@log_type = log_type
super
end
def display_actions?
@ -24,7 +23,7 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def create_button_href
send("#{log_type}_logs_path")
helpers.send("#{log_type}_logs_path")
end
def upload_button_copy
@ -32,23 +31,23 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def upload_button_href
send("bulk_upload_#{log_type}_log_path", id: "start")
helpers.send("bulk_upload_#{log_type}_log_path", id: "start")
end
def create_test_log_href
send("create_test_#{log_type}_log_path")
helpers.send("create_test_#{log_type}_log_path")
end
def create_next_year_test_log_href
send("create_next_year_test_#{log_type}_log_path")
helpers.send("create_next_year_test_#{log_type}_log_path")
end
def create_setup_test_log_href
send("create_setup_test_#{log_type}_log_path")
helpers.send("create_setup_test_#{log_type}_log_path")
end
def create_next_year_setup_test_log_href
send("create_next_year_setup_test_#{log_type}_log_path")
helpers.send("create_next_year_setup_test_#{log_type}_log_path")
end
def current_collection_year_label
@ -60,7 +59,7 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def create_test_bulk_upload_href(year)
send("create_#{year}_test_#{log_type}_bulk_upload_path")
helpers.send("create_#{year}_test_#{log_type}_bulk_upload_path")
end
def view_uploads_button_copy
@ -68,6 +67,6 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def view_uploads_button_href
send("bulk_uploads_#{log_type}_logs_path")
helpers.send("bulk_uploads_#{log_type}_logs_path")
end
end

2
app/components/data_protection_confirmation_banner_component.html.erb

@ -1,5 +1,5 @@
<% if display_banner? %>
<%= govuk_notification_banner(title_text: "Important") do %>
<%= helpers.govuk_notification_banner(title_text: "Important") do %>
<p class="govuk-notification-banner__heading govuk-!-width-full" style="max-width: fit-content">
<%= header_text %>
<p>

5
app/components/data_protection_confirmation_banner_component.rb

@ -4,10 +4,9 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
attr_reader :user, :organisation
def initialize(user:, organisation: nil)
super()
@user = user
@organisation = organisation
super
end
def display_banner?
@ -32,7 +31,7 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
def banner_text
if show_no_dpo_message? || user.is_dpo? || !org_or_user_org.holds_own_stock?
govuk_link_to(
helpers.govuk_link_to(
link_text,
link_href,
class: "govuk-notification-banner__link govuk-!-font-weight-bold",

2
app/components/document_list_component.html.erb

@ -5,7 +5,7 @@
<% items.each do |item| %>
<div class="app-document-list__item">
<dt class="app-document-list__item-title">
<%= govuk_link_to item[:name], item[:href] %>
<%= helpers.govuk_link_to item[:name], item[:href] %>
</dt>
<% if item[:description] %>
<dd class="app-document-list__item-description"><%= item[:description] %></dd>

2
app/components/document_list_component.rb

@ -2,8 +2,8 @@ class DocumentListComponent < ViewComponent::Base
attr_reader :items, :label
def initialize(items:, label:)
super()
@items = items
@label = label
super
end
end

2
app/components/lettings_log_summary_component.html.erb

@ -3,7 +3,7 @@
<div class="govuk-grid-column-two-thirds">
<header class="app-log-summary__header">
<h2 class="app-log-summary__title">
<%= govuk_link_to lettings_log_path(log) do %>
<%= helpers.govuk_link_to lettings_log_path(log) do %>
Log <%= log.id %>
<% end %>
</h2>

2
app/components/lettings_log_summary_component.rb

@ -2,9 +2,9 @@ class LettingsLogSummaryComponent < ViewComponent::Base
attr_reader :current_user, :log
def initialize(current_user:, log:)
super()
@current_user = current_user
@log = log
super
end
def log_status

2
app/components/missing_stock_owners_banner_component.html.erb

@ -1,5 +1,5 @@
<% if display_banner? %>
<%= govuk_notification_banner(title_text: "Important") do %>
<%= helpers.govuk_notification_banner(title_text: "Important") do %>
<p class="govuk-notification-banner__heading govuk-!-width-full" style="max-width: fit-content">
<%= header_text %>
<p>

9
app/components/missing_stock_owners_banner_component.rb

@ -4,10 +4,9 @@ class MissingStockOwnersBannerComponent < ViewComponent::Base
attr_reader :user, :organisation
def initialize(user:, organisation: nil)
super()
@user = user
@organisation = organisation || user.organisation
super
end
def display_banner?
@ -36,7 +35,7 @@ class MissingStockOwnersBannerComponent < ViewComponent::Base
private
def add_stock_owner_link
govuk_link_to(
helpers.govuk_link_to(
"add a stock owner",
stock_owners_add_organisation_path(id: organisation.id),
class: "govuk-notification-banner__link govuk-!-font-weight-bold",
@ -44,7 +43,7 @@ private
end
def contact_helpdesk_link
govuk_link_to(
helpers.govuk_link_to(
"contact the helpdesk",
GlobalConstants::HELPDESK_URL,
class: "govuk-notification-banner__link govuk-!-font-weight-bold",
@ -52,7 +51,7 @@ private
end
def users_link
govuk_link_to(
helpers.govuk_link_to(
"users page",
users_path,
class: "govuk-notification-banner__link govuk-!-font-weight-bold",

2
app/components/primary_navigation_component.html.erb

@ -1,4 +1,4 @@
<%= govuk_service_navigation(navigation_id: "primary-navigation", classes: "app-service-navigation") do |sn|
<%= helpers.govuk_service_navigation(navigation_id: "primary-navigation", classes: "app-service-navigation") do |sn|
items.each do |item|
sn.with_navigation_item(text: item[:text], href: item[:href], classes: "", current: item[:current])
end

2
app/components/primary_navigation_component.rb

@ -2,8 +2,8 @@ class PrimaryNavigationComponent < ViewComponent::Base
attr_reader :items
def initialize(items:)
super()
@items = items
super
end
def highlighted_item?(item, _path)

2
app/components/sales_log_summary_component.html.erb

@ -3,7 +3,7 @@
<div class="govuk-grid-column-two-thirds">
<header class="app-log-summary__header">
<h2 class="app-log-summary__title">
<%= govuk_link_to sales_log_path(log) do %>
<%= helpers.govuk_link_to sales_log_path(log) do %>
Log <%= log.id %>
<% end %>
</h2>

2
app/components/sales_log_summary_component.rb

@ -2,9 +2,9 @@ class SalesLogSummaryComponent < ViewComponent::Base
attr_reader :current_user, :log
def initialize(current_user:, log:)
super()
@current_user = current_user
@log = log
super
end
def log_status

4
app/components/search_component.html.erb

@ -1,4 +1,4 @@
<%= form_with url: path(current_user), method: "get", local: true do |f| %>
<%= helpers.form_with url: path(current_user), method: "get", local: true do |f| %>
<div class="app-search govuk-!-margin-bottom-4">
<%= f.govuk_text_field :search,
form_group: {
@ -11,6 +11,6 @@
class: "app-search__input" %>
<%= f.govuk_submit "Search", class: "app-search__button" %>
<%= govuk_button_link_to "Clear search", path(current_user), secondary: true, class: "app-search__button" %>
<%= helpers.govuk_button_link_to "Clear search", path(current_user), secondary: true, class: "app-search__button" %>
</div>
<% end %>

2
app/components/search_component.rb

@ -2,10 +2,10 @@ class SearchComponent < ViewComponent::Base
attr_reader :current_user, :search_label, :value
def initialize(current_user:, search_label:, value: nil)
super()
@current_user = current_user
@search_label = search_label
@value = value
super
end
def path(current_user)

2
app/components/search_result_caption_component.rb

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

4
app/components/sub_navigation_component.html.erb

@ -3,11 +3,11 @@
<% items.each do |item| %>
<% if item.current %>
<li class="app-sub-navigation__item app-sub-navigation__item--current">
<%= govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link", aria: { current: "page" } %>
<%= helpers.govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link", aria: { current: "page" } %>
</li>
<% else %>
<li class="app-sub-navigation__item">
<%= govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link" %>
<%= helpers.govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link" %>
</li>
<% end %>
<% end %>

2
app/components/sub_navigation_component.rb

@ -2,8 +2,8 @@ class SubNavigationComponent < ViewComponent::Base
attr_reader :items
def initialize(items:)
super()
@items = items
super
end
def highlighted_item?(item, _path)

6
app/controllers/auth/confirmations_controller.rb

@ -5,7 +5,11 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
yield resource if block_given?
if resource.errors.empty?
if resource.sign_in_count.zero?
# previously we reset sign_in_count on deactivation and had only the .zero? check here.
# this would force a password reset both if it was your very first log in, and on your first login after reactivation.
# now we have a specific flag for the latter case as resetting sign_in_count was difficult for auditing.
# note that some deactivated users will have a sign_in_count of 0 and not have this flag set if they were deactivated before we made this change.
if resource.force_reset_password_on_confirmation || resource.sign_in_count.zero?
token = resource.send(:set_reset_password_token)
redirect_to "#{edit_user_password_url}?reset_password_token=#{token}&confirmation=true"
else

1
app/controllers/auth/passwords_controller.rb

@ -37,6 +37,7 @@ class Auth::PasswordsController < Devise::PasswordsController
if resource.errors.empty?
resource.unlock_access! if resource.respond_to?(:unlock_access!)
resource.force_reset_password_on_confirmation = false
if Devise.sign_in_after_reset_password
set_flash_message!(:notice, password_update_flash_message)
resource.after_database_authentication

10
app/controllers/users_controller.rb

@ -221,8 +221,14 @@ private
@user.errors.add :phone
end
if user_params.key?(:organisation_id) && user_params[:organisation_id].blank?
@user.errors.add :organisation_id, :blank
if user_params.key?(:organisation_id)
if user_params[:organisation_id].blank?
@user.errors.add :organisation_id, :blank
elsif !@user.role_is_allowed_to_be_in_organisation?(override_organisation_id: user_params[:organisation_id].to_i) && @user.id.present?
# this will also be flagged by the validation in user.rb.
# for convenience we show the error early before they go through the change org flow (involves reassigning logs).
@user.errors.add :organisation_id, I18n.t("validations.user.support_user_in_wrong_organisation.change_organisation")
end
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

2
app/frontend/styles/_filter.scss

@ -109,7 +109,7 @@
}
.autocomplete__option__hint {
@include govuk-font(14);
@include govuk-font(16);
word-break: break-all;
}
}

11
app/frontend/styles/_header.scss

@ -1,6 +1,4 @@
.app-header {
border-bottom: govuk-spacing(2) solid $govuk-brand-colour;
.govuk-header__logo {
@include govuk-media-query($from: desktop) {
width: 60%;
@ -21,12 +19,3 @@
}
}
}
.app-header--orange,
.app-header--orange .govuk-header__container {
border-bottom-color: govuk-colour("orange");
}
.app-header__no-border-bottom {
border-bottom: 0;
}

2
app/frontend/styles/_related-navigation.scss

@ -11,7 +11,7 @@
.app-related-navigation__sub-heading {
@include govuk-font(16);
border-top: 1px solid govuk-colour("mid-grey", $legacy: "grey-2");
border-top: 1px solid govuk-colour("mid-grey");
margin: 0;
padding-top: govuk-spacing(3);
}

2
app/frontend/styles/_tag.scss

@ -1,5 +1,5 @@
.app-tag--small {
@include govuk-font(14, $weight: bold);
@include govuk-font(16, $weight: bold);
padding-top: 2px;
padding-right: 6px;
padding-bottom: 2px;

2
app/frontend/styles/_testing-tools.scss

@ -12,7 +12,7 @@
}
.app-testing-tools__tag {
@include govuk-font(14);
@include govuk-font(16);
background-color: #fcd6c3;
margin-top: 0;
margin-bottom: 10px;

12
app/frontend/styles/application.scss

@ -130,3 +130,15 @@ $govuk-breakpoints: (
left: -15px;
position: relative;
}
.app-main-service-navigation {
border-bottom: govuk-spacing(2) solid $govuk-brand-colour;
}
.app-service-navigation--orange {
border-bottom-color: govuk-colour("orange");
}
.app-service-navigation--no-border {
border-bottom: 0;
}

13
app/helpers/application_helper.rb

@ -10,14 +10,11 @@ module ApplicationHelper
end
end
def govuk_header_classes(current_user)
if current_user&.support?
"app-header app-header--orange"
elsif notifications_to_display?
"app-header app-header__no-border-bottom"
else
"app-header"
end
def govuk_service_navigation_classes(current_user)
return "app-service-navigation--orange" if current_user&.support?
return "app-service-navigation--no-border" if notifications_to_display?
""
end
def govuk_phase_banner_tag(current_user)

2
app/models/derived_variables/lettings_log_variables.rb

@ -339,7 +339,7 @@ private
def infer_only_partner!(partner_number)
return unless hhmemb
(2..hhmemb).each do |i|
(2..people_with_details).each do |i|
next if i == partner_number
if ["P", nil].include?(public_send("relat#{i}"))

25
app/models/form/lettings/pages/net_income_value_check.rb

@ -1,9 +1,9 @@
class Form::Lettings::Pages::NetIncomeValueCheck < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "net_income_value_check"
def initialize(id, hsh, subsection, person_index: nil)
super(id, hsh, subsection)
@copy_key = "lettings.soft_validations.net_income_value_check"
@depends_on = [{ "net_income_soft_validation_triggered?" => true }]
@person_index = person_index
@depends_on = depends_on
@title_text = {
"translation" => "forms.#{form.start_date.year}.#{@copy_key}.title_text",
"arguments" => [
@ -32,6 +32,23 @@ class Form::Lettings::Pages::NetIncomeValueCheck < ::Form::Page
}
end
def depends_on
if @person_index.present?
[
{
"net_income_soft_validation_triggered?" => true,
"details_known_#{@person_index}" => 0,
},
]
else
[
{
"net_income_soft_validation_triggered?" => true,
},
]
end
end
def questions
@questions ||= [Form::Lettings::Questions::NetIncomeValueCheck.new(nil, nil, self)]
end

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

9
app/models/form/lettings/pages/person_known.rb

@ -2,11 +2,18 @@ class Form::Lettings::Pages::PersonKnown < ::Form::Page
def initialize(id, hsh, subsection, person_index:)
super(id, hsh, subsection)
@id = "person_#{person_index}_known"
@depends_on = (person_index..8).map { |index| { "hhmemb" => index } }
@person_index = person_index
@depends_on = depends_on
end
def questions
@questions ||= [Form::Lettings::Questions::DetailsKnown.new(nil, nil, self, person_index: @person_index)]
end
def depends_on
[{ "hhmemb" => {
"operator" => ">=",
"operand" => @person_index,
} }]
end
end

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

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

@ -5,7 +5,7 @@ class Form::Lettings::Questions::Hhmemb < ::Form::Question
@type = "numeric"
@width = 2
@check_answers_card_number = 0
@max = 8
@max = 15
@min = 1
@step = 1
@question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR)

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

@ -13,10 +13,11 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
(Form::Lettings::Pages::NoFemalesPregnantHouseholdLeadHhmembValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdLeadHhmembValueCheck.new(nil, nil, self) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_hhmemb_check", nil, self) if form.start_year_2026_or_later?),
Form::Lettings::Pages::NetIncomeValueCheck.new("hhmemb_net_income_value_check", 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::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?),
@ -37,6 +38,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::LeadTenantUnderRetirementValueCheck.new("working_situation_lead_tenant_under_retirement_value_check", nil, self),
Form::Lettings::Pages::LeadTenantOverRetirementValueCheck.new("working_situation_lead_tenant_over_retirement_value_check", nil, self),
(Form::Lettings::Pages::WorkingSituationIllnessCheckLead.new("working_situation_lead_tenant_long_term_illness_check", nil, self) if form.start_year_2026_or_later?),
Form::Lettings::Pages::NetIncomeValueCheck.new("working_situation_lead_tenant_net_income_value_check", nil, self),
*person_questions(person_index: 2),
*person_questions(person_index: 3),
*person_questions(person_index: 4),
@ -50,10 +52,14 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
def person_questions(person_index:)
[
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index:),
(Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::NetIncomeValueCheck.new("age_#{person_index}_net_income_value_check", nil, self, person_index:) if form.start_year_2026_or_later?),
relationship_question(person_index:),
(Form::Lettings::Pages::PartnerUnder16ValueCheck.new("relationship_#{person_index}_partner_under_16_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::MultiplePartnersValueCheck.new("relationship_#{person_index}_multiple_partners_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
@ -61,6 +67,8 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonUnderRetirementValueCheck.new("age_#{person_index}_under_retirement_value_check", nil, self, person_index:),
Form::Lettings::Pages::PersonOverRetirementValueCheck.new("age_#{person_index}_over_retirement_value_check", nil, self, person_index:),
(Form::Lettings::Pages::PartnerUnder16ValueCheck.new("age_#{person_index}_partner_under_16_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NetIncomeValueCheck.new("age_#{person_index}_net_income_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonSexRegisteredAtBirth.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonGenderSameAsSex.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonGenderIdentity.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
@ -68,10 +76,12 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdPersonValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_person_#{person_index}_check", nil, self, person_index:) if form.start_year_2026_or_later?),
Form::Lettings::Pages::PersonOverRetirementValueCheck.new("gender_#{person_index}_over_retirement_value_check", nil, self, person_index:),
Form::Lettings::Pages::PersonWorkingSituation.new(nil, nil, self, person_index:),
Form::Lettings::Pages::PersonUnderRetirementValueCheck.new("working_situation_#{person_index}_under_retirement_value_check", nil, self, person_index:),
Form::Lettings::Pages::PersonOverRetirementValueCheck.new("working_situation_#{person_index}_over_retirement_value_check", nil, self, person_index:),
(Form::Lettings::Pages::WorkingSituationIllnessCheckPerson.new("working_situation_#{person_index}_long_term_illness_check", nil, self, person_index:) if form.start_year_2026_or_later?),
Form::Lettings::Pages::NetIncomeValueCheck.new("working_situation_#{person_index}_net_income_value_check", nil, self, person_index:),
]
end

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

@ -10,7 +10,7 @@ class Form::Lettings::Subsections::IncomeAndBenefits < ::Form::Subsection
@pages ||= [
Form::Lettings::Pages::IncomeKnown.new(nil, nil, self),
Form::Lettings::Pages::IncomeAmount.new(nil, nil, self),
Form::Lettings::Pages::NetIncomeValueCheck.new(nil, nil, self),
Form::Lettings::Pages::NetIncomeValueCheck.new("income_amount_net_income_value_check", nil, self),
Form::Lettings::Pages::HousingBenefit.new("housing_benefit", nil, self),
Form::Lettings::Pages::BenefitsProportion.new("benefits_proportion", nil, self),
Form::Lettings::Pages::RentOrOtherCharges.new(nil, nil, self),

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

1
app/models/form/sales/questions/building_height_class.rb

@ -3,6 +3,7 @@ class Form::Sales::Questions::BuildingHeightClass < ::Form::Question
super
@id = "buildheightclass"
@type = "radio"
@top_guidance_partial = "building_height_class"
@answer_options = ANSWER_OPTIONS
@question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR)
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

1
app/models/form/sales/questions/management_fee.rb

@ -5,6 +5,7 @@ class Form::Sales::Questions::ManagementFee < ::Form::Question
@copy_key = "sales.sale_information.management_fee.management_fee"
@type = "numeric"
@min = 1
@max = form.start_year_2025_or_later? ? 9_999 : nil
@step = 0.01
@width = 5
@prefix = "£"

1
app/models/form/sales/questions/monthly_rent_before_staircasing.rb

@ -5,6 +5,7 @@ class Form::Sales::Questions::MonthlyRentBeforeStaircasing < ::Form::Question
@copy_key = "sales.sale_information.mrent_staircasing.prestaircasing"
@type = "numeric"
@min = 0
@max = form.start_year_2025_or_later? ? 9_999 : nil
@step = 0.01
@width = 5
@prefix = "£"

1
app/models/form/sales/questions/mortgage_amount.rb

@ -4,6 +4,7 @@ class Form::Sales::Questions::MortgageAmount < ::Form::Question
@id = "mortgage"
@type = "numeric"
@min = 1
@max = form.start_year_2025_or_later? ? 999_999 : nil
@step = 1
@width = 5
@prefix = "£"

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

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

@ -4,7 +4,8 @@ class Form::Sales::Questions::PurchasePrice < ::Form::Question
@id = "value"
@type = "numeric"
@min = form.start_year_2026_or_later? ? 15_000 : 0
@step = 0.01
@max = form.start_year_2025_or_later? ? 999_999 : nil
@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

1
app/models/form/sales/questions/value.rb

@ -5,6 +5,7 @@ class Form::Sales::Questions::Value < ::Form::Question
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.value.#{page.id}" : "sales.sale_information.value"
@type = "numeric"
@min = form.start_year_2026_or_later? ? 15_000 : 0
@max = form.start_year_2025_or_later? ? 999_999 : nil
@step = 1
@width = 10
@prefix = "£"

6
app/models/forms/bulk_upload_resume/confirm.rb

@ -20,11 +20,11 @@ module Forms
send("resume_bulk_upload_#{log_type}_result_path", bulk_upload)
end
def error_report_path
def error_report_path(read_only: false)
if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload)
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
else
send("bulk_upload_#{log_type}_result_path", bulk_upload)
send("bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
end
end

6
app/models/forms/bulk_upload_resume/fix_choice.rb

@ -34,11 +34,11 @@ module Forms
end
end
def error_report_path
def error_report_path(read_only: false)
if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload)
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
else
send("bulk_upload_#{log_type}_result_path", bulk_upload)
send("bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
end
end

24
app/models/lettings_log.rb

@ -191,6 +191,7 @@ class LettingsLog < Log
NUM_OF_WEEKS_FROM_PERIOD = { 2 => 26, 3 => 13, 4 => 12, 5 => 50, 6 => 49, 7 => 48, 8 => 47, 9 => 46, 11 => 51, 1 => 52, 10 => 53 }.freeze
SUFFIX_FROM_PERIOD = { 2 => "every 2 weeks", 3 => "every 4 weeks", 4 => "every month" }.freeze
DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id tenancycode startdate age1_known age1 sex1 sexrab1 ecstat1 tcharge household_charge chcharge].freeze
MAX_PEOPLE_WITH_DETAILS = 8 # This is not yet used in all lettings validations etc. so check for other occurrences of this concept if updating this
RENT_TYPE = {
social_rent: 0,
affordable_rent: 1,
@ -284,7 +285,7 @@ class LettingsLog < Log
range = ALLOWED_INCOME_RANGES[ecstat1].clone
if hhmemb > 1
(2..hhmemb).each do |person_index|
(2..people_with_details).each do |person_index|
ecstat = self["ecstat#{person_index}"]
if ecstat.nil?
@ -542,7 +543,7 @@ class LettingsLog < Log
reason == 1
end
def receives_housing_benefit_only?
def receives_housing_benefit?
# 1: Housing benefit
hb == 1
end
@ -551,13 +552,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 +567,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?

8
app/models/log.rb

@ -204,6 +204,14 @@ class Log < ApplicationRecord
false
end
def people_with_details
[hhmemb || max_people_with_details, max_people_with_details].min
end
def max_people_with_details
self.class::MAX_PEOPLE_WITH_DETAILS
end
def ethnic_refused?
ethnic_group == 17
end

1
app/models/sales_log.rb

@ -104,6 +104,7 @@ class SalesLog < Log
OPTIONAL_FIELDS = %w[purchid othtype buyers_organisations].freeze
DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id purchid saledate age1_known age1 sex1 sexrab1 ecstat1 postcode_full uprn address_line1].freeze
MAX_PEOPLE_WITH_DETAILS = 6 # This is not yet used in all sales validations etc. so check for other occurrences of this concept if updating this
def lettings?
false

19
app/models/user.rb

@ -25,6 +25,7 @@ class User < ApplicationRecord
validates :organisation_id, presence: true
validate :organisation_not_merged
validate :support_user_is_in_correct_organisation
has_paper_trail ignore: %w[last_sign_in_at
current_sign_in_at
@ -179,7 +180,7 @@ class User < ApplicationRecord
update!(
active: false,
confirmed_at: nil,
sign_in_count: 0,
force_reset_password_on_confirmation: true,
initial_confirmation_sent: false,
reactivate_with_organisation:,
unconfirmed_email: nil,
@ -390,6 +391,12 @@ class User < ApplicationRecord
end
end
def role_is_allowed_to_be_in_organisation?(override_organisation_id: nil)
return true unless support? && FeatureToggle.support_organisation_allow_list.present?
FeatureToggle.support_organisation_allow_list.include?(override_organisation_id || organisation_id)
end
protected
# Checks whether a password is needed or not. For validations only.
@ -407,6 +414,16 @@ private
end
end
def support_user_is_in_correct_organisation
return if role_is_allowed_to_be_in_organisation?
if role_changed?
errors.add :role, I18n.t("validations.user.support_user_in_wrong_organisation.change_role")
else
errors.add :organisation_id, I18n.t("validations.user.support_user_in_wrong_organisation.change_organisation")
end
end
def send_data_protection_confirmation_reminder
return unless persisted?
return unless is_dpo?

13
app/models/validations/financial_validations.rb

@ -41,7 +41,7 @@ module Validations::FinancialValidations
:over_hard_max,
message: I18n.t("validations.lettings.financial.hhmemb.earnings_over_hard_max", earnings: format_as_currency(record.earnings), frequency:),
)
(1..record.hhmemb).each do |n|
(1..record.people_with_details).each do |n|
record.errors.add(
"ecstat#{n}",
:over_hard_max,
@ -70,7 +70,7 @@ module Validations::FinancialValidations
:under_hard_min,
message: I18n.t("validations.lettings.financial.hhmemb.earnings_under_hard_min", earnings: format_as_currency(record.earnings), frequency:),
)
(1..record.hhmemb).each do |n|
(1..record.people_with_details).each do |n|
record.errors.add(
"ecstat#{n}",
:under_hard_min,
@ -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)

6
app/models/validations/sales/sale_information_validations.rb

@ -42,7 +42,7 @@ module Validations::Sales::SaleInformationValidations
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_after_1980")
end
if record.saledate.present? && record.initialpurchase > record.saledate
if record.saledate.present? && ((record.initialpurchase > record.saledate) || (record.initialpurchase == record.saledate && record.form.start_year_2026_or_later?))
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_before_saledate")
record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.sale_information.saledate.must_be_after_initial_purchase_date")
end
@ -55,11 +55,11 @@ module Validations::Sales::SaleInformationValidations
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_1980")
end
if record.saledate.present? && record.lasttransaction > record.saledate
if record.saledate.present? && ((record.lasttransaction > record.saledate) || (record.lasttransaction == record.saledate && record.form.start_year_2026_or_later?))
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_before_saledate")
record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.sale_information.saledate.must_be_after_last_transaction_date")
end
if record.initialpurchase.present? && record.lasttransaction < record.initialpurchase
if record.initialpurchase.present? && ((record.lasttransaction < record.initialpurchase) || (record.lasttransaction == record.initialpurchase && record.form.start_year_2026_or_later?))
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_before_last_transaction")
record.errors.add :lasttransaction, I18n.t("validations.sales.sale_information.lasttransaction.must_be_after_initial_purchase")
end

35
app/models/validations/soft_validations.rb

@ -208,8 +208,7 @@ module Validations::SoftValidations
def multiple_partners?
return unless hhmemb
max_person_with_details = sales? ? [hhmemb, 6].min : [hhmemb, 8].min
(2..max_person_with_details).many? { |n| public_send("relat#{n}") == "P" }
(2..people_with_details).many? { |n| public_send("relat#{n}") == "P" }
end
def at_least_one_working_situation_is_sickness_and_household_sickness_is_no?
@ -219,22 +218,18 @@ module Validations::SoftValidations
private
def all_tenants_age_and_gender_information_completed?
return false if hhmemb.present? && hhmemb > 8
return false if hhmemb.present? && hhmemb > max_people_with_details
return false unless all_tenants_gender_information_completed?
person_count = hhmemb || 8
(1..person_count).all? do |n|
(1..people_with_details).all? do |n|
public_send("age#{n}").present? && public_send("age#{n}_known").present? && public_send("age#{n}_known").zero?
end
end
def all_tenants_gender_information_completed?
return false if hhmemb.present? && hhmemb > 8
person_count = hhmemb || 8
return false if hhmemb.present? && hhmemb > max_people_with_details
(1..person_count).all? do |n|
(1..people_with_details).all? do |n|
tenant_gender_information_completed?(n)
end
end
@ -258,27 +253,21 @@ private
end
def any_non_male_in_expected_pregnancy_age_range(min, max)
person_count = hhmemb || 8
(1..person_count).any? do |n|
(1..people_with_details).any? do |n|
person_in_expected_pregnancy_age_range(n, min, max) && person_is_non_male(n)
end
end
def non_males_in_the_household?
person_count = hhmemb || 8
(1..person_count).any? do |n|
(1..people_with_details).any? do |n|
person_is_non_male(n)
end
end
def all_male_tenants_in_the_household?
return false if hhmemb.present? && hhmemb > 8
return false if hhmemb.present? && hhmemb > max_people_with_details
person_count = hhmemb || 8
(1..person_count).all? do |n|
(1..people_with_details).all? do |n|
person_is_male(n)
end
end
@ -344,11 +333,7 @@ private
end
def at_least_one_person_working_situation_is_illness?
return if hhmemb.present? && hhmemb > 8
person_count = hhmemb || 8
(1..person_count).any? { |n| public_send("ecstat#{n}") == 8 }
(1..people_with_details).any? { |n| public_send("ecstat#{n}") == 8 }
end
def no_one_in_household_with_illness?

11
app/services/bulk_upload/lettings/year2025/row_parser.rb

@ -461,7 +461,6 @@ class BulkUpload::Lettings::Year2025::RowParser
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: -> { supported_housing? }
validate :validate_address_fields, on: :after_log, unless: -> { supported_housing? }
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_nationality, on: :after_log
validate :validate_reasonpref_reason_values, on: :after_log
validate :validate_prevten_value_when_renewal, on: :after_log
@ -506,6 +505,8 @@ class BulkUpload::Lettings::Year2025::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
@valid = errors.blank?
@ -1035,6 +1036,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end

11
app/services/bulk_upload/lettings/year2026/row_parser.rb

@ -496,7 +496,6 @@ class BulkUpload::Lettings::Year2026::RowParser
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, on: :after_log
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_nationality, on: :after_log
validate :validate_reasonpref_reason_values, on: :after_log
validate :validate_prevten_value_when_renewal, on: :after_log
@ -541,6 +540,8 @@ class BulkUpload::Lettings::Year2026::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
@valid = errors.blank?
@ -1113,6 +1114,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end

30
app/services/bulk_upload/sales/year2025/row_parser.rb

@ -154,6 +154,7 @@ class BulkUpload::Sales::Year2025::RowParser
:field_52, # Gender identity of person 5
:field_56, # Gender identity of person 6
:field_58, # What was buyer 1’s previous tenure?
:field_64, # What was buyer 2’s previous tenure?
:field_75, # What is the total amount the buyers had in savings before they paid any deposit for the property?
@ -227,7 +228,7 @@ class BulkUpload::Sales::Year2025::RowParser
attribute :field_56, :string
attribute :field_57, :integer
attribute :field_58, :integer
attribute :field_58, :string
attribute :field_59, :integer
attribute :field_60, :string
attribute :field_61, :string
@ -438,8 +439,6 @@ class BulkUpload::Sales::Year2025::RowParser
validate :validate_assigned_to_when_support, on: :after_log
validate :validate_managing_org_related, on: :after_log
validate :validate_relevant_collection_window, on: :after_log
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, on: :after_log
validate :validate_if_log_already_exists, on: :after_log, if: -> { FeatureToggle.bulk_upload_duplicate_log_check_enabled? }
@ -503,6 +502,8 @@ class BulkUpload::Sales::Year2025::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
errors.blank?
@ -564,6 +565,15 @@ private
end
end
def prevten
case field_58
when "R"
0
else
field_58
end
end
def prevtenbuy2
case field_64
when "R"
@ -689,6 +699,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end
@ -902,7 +920,7 @@ private
attributes["savings"] = field_75.to_i if attributes["savingsnk"]&.zero? && field_75&.match(/\A\d+\z/)
attributes["prevown"] = field_76
attributes["prevten"] = field_58
attributes["prevten"] = prevten
attributes["prevloc"] = field_62
attributes["previous_la_known"] = previous_la_known
attributes["ppcodenk"] = previous_postcode_known
@ -1283,7 +1301,7 @@ private
def infer_soctenant_from_prevten_and_prevtenbuy2
return unless shared_ownership?
if [1, 2].include?(field_58) || [1, 2].include?(field_64.to_i)
if [1, 2].include?(field_58.to_i) || [1, 2].include?(field_64.to_i)
1
else
2
@ -1501,7 +1519,7 @@ private
next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) }
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end

29
app/services/bulk_upload/sales/year2026/row_parser.rb

@ -169,6 +169,7 @@ class BulkUpload::Sales::Year2026::RowParser
:field_61, # Person 5's sex, as registered at birth
:field_67, # Person 6's sex, as registered at birth
:field_71, # What was buyer 1’s previous tenure?
:field_77, # What was buyer 2’s previous tenure?
:field_88, # What is the total amount the buyers had in savings before they paid any deposit for the property?
@ -263,7 +264,7 @@ class BulkUpload::Sales::Year2026::RowParser
attribute :field_69, :string
attribute :field_70, :integer
attribute :field_71, :integer
attribute :field_71, :string
attribute :field_72, :integer
attribute :field_73, :string
attribute :field_74, :string
@ -492,7 +493,6 @@ class BulkUpload::Sales::Year2026::RowParser
validate :validate_assigned_to_when_support, on: :after_log
validate :validate_managing_org_related, on: :after_log
validate :validate_relevant_collection_window, on: :after_log
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
validate :validate_address_fields, on: :after_log
@ -560,6 +560,8 @@ class BulkUpload::Sales::Year2026::RowParser
end
end
validate_incomplete_soft_validations
add_errors_for_invalid_fields
errors.blank?
@ -621,6 +623,15 @@ private
end
end
def prevten
case field_71
when "R"
0
else
field_71
end
end
def prevtenbuy2
case field_77
when "R"
@ -750,6 +761,14 @@ private
def add_errors_for_invalid_fields
invalid_fields.each do |field|
# ensure questions not routed to are not included in error report
error_questions_ids = field_mapping_for_errors
.select { |_k, fields| fields.map(&:to_s).include?(field.to_s) }
.keys
.map(&:to_s)
error_questions = questions.select { |question| error_questions_ids.include?(question.id) }
next if error_questions.none? { |question| question.page.routed_to?(log, nil) }
errors.delete(field) # take precedence over any other errors as this is a BU format issue
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: QUESTIONS[field.to_sym]))
end
@ -995,7 +1014,7 @@ private
attributes["savings"] = field_88.to_i if attributes["savingsnk"]&.zero? && field_88&.match(/\A\d+\z/)
attributes["prevown"] = field_89
attributes["prevten"] = field_71
attributes["prevten"] = prevten
attributes["prevloc"] = field_75
attributes["previous_la_known"] = previous_la_known
attributes["ppcodenk"] = previous_postcode_known
@ -1416,7 +1435,7 @@ private
def infer_soctenant_from_prevten_and_prevtenbuy2
return unless shared_ownership?
if [1, 2].include?(field_71) || [1, 2].include?(field_77.to_i)
if [1, 2].include?(field_71.to_i) || [1, 2].include?(field_77.to_i)
1
else
2
@ -1654,7 +1673,7 @@ private
next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) }
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end

6
app/services/feature_toggle.rb

@ -34,4 +34,10 @@ class FeatureToggle
def self.sales_export_enabled?
Time.zone.now >= Time.zone.local(2025, 4, 1) || (Rails.env.review? || Rails.env.staging?)
end
# IDs of organisations a user must be in to be allowed the support role
# if nil this feature will be disabled
def self.support_organisation_allow_list
[1] if Rails.env.production?
end
end

4
app/views/bulk_upload_lettings_results/show.html.erb

@ -33,4 +33,6 @@
</div>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

8
app/views/bulk_upload_lettings_results/summary.html.erb

@ -1,3 +1,7 @@
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<% end %>
<%= render partial: "bulk_upload_shared/moved_user_banner" %>
<div class="govuk-grid-row">
@ -34,4 +38,6 @@
<% end %>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

2
app/views/bulk_upload_lettings_resume/confirm.html.erb

@ -9,7 +9,7 @@
<p class="govuk-body">
<%= logs_and_errors_warning(@bulk_upload) %>
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</p>
<% if unique_answers_to_be_cleared(@bulk_upload).present? %>

2
app/views/bulk_upload_lettings_resume/fix_choice.html.erb

@ -19,7 +19,7 @@
</div>
<div class="govuk-body">
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</div>
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %>

4
app/views/bulk_upload_sales_results/show.html.erb

@ -33,4 +33,6 @@
</div>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

8
app/views/bulk_upload_sales_results/summary.html.erb

@ -1,3 +1,7 @@
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<% end %>
<%= render partial: "bulk_upload_shared/moved_user_banner" %>
<div class="govuk-grid-row">
@ -34,4 +38,6 @@
<% end %>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

2
app/views/bulk_upload_sales_resume/confirm.html.erb

@ -9,7 +9,7 @@
<p class="govuk-body">
<%= logs_and_errors_warning(@bulk_upload) %>
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</p>
<% if unique_answers_to_be_cleared(@bulk_upload).present? %>

2
app/views/bulk_upload_sales_resume/fix_choice.html.erb

@ -19,7 +19,7 @@
</div>
<div class="govuk-body">
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</div>
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %>

5
app/views/form/guidance/_building_height_class.html.erb

@ -0,0 +1,5 @@
<div class="govuk-body">
<%= govuk_details(summary_text: I18n.t("forms.#{@log.form.start_date.year}.sales.guidance.building_height_class.title")) do %>
<%= I18n.t("forms.#{@log.form.start_date.year}.sales.guidance.building_height_class.content").html_safe %>
<% end %>
</div>

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/layouts/application.html.erb

@ -81,20 +81,24 @@
<%= govuk_skip_link %>
<%= govuk_header(
classes: govuk_header_classes(current_user),
classes: "app-header",
homepage_url: root_path,
navigation_classes: "govuk-header__navigation--end",
) do |component|
component.with_product_name(name: t("service_name"))
unless FeatureToggle.service_moved? || FeatureToggle.service_unavailable?
if current_user.nil?
component.with_navigation_item(text: "Sign in", href: user_session_path)
else
component.with_navigation_item(text: "Your account", href: account_path)
component.with_navigation_item(text: "Sign out", href: destroy_user_session_path)
end
end
end %>
) %>
<%= govuk_service_navigation(
service_name: t("service_name"),
service_url: root_path,
classes: "app-main-service-navigation #{govuk_service_navigation_classes(current_user)}",
) do |component| %>
<% unless FeatureToggle.service_moved? || FeatureToggle.service_unavailable? %>
<% if current_user.nil? %>
<%= component.with_navigation_item(text: "Sign in", href: user_session_path) %>
<% else %>
<%= component.with_navigation_item(text: "Your account", href: account_path) %>
<%= component.with_navigation_item(text: "Sign out", href: destroy_user_session_path) %>
<% end %>
<% end %>
<% end %>
<% if notifications_to_display? %>
<%= render "notifications/notification_banner" %>

18
app/views/layouts/rails_admin/_navigation.html.erb

@ -1,9 +1,13 @@
<%= govuk_header(
classes: "app-header app-header--orange",
classes: "app-header",
homepage_url: Rails.application.routes.url_helpers.root_path,
navigation_classes: "govuk-header__navigation--end",
) do |component|
component.with_product_name(name: t("service_name"))
component.with_navigation_item(text: "Your account", href: Rails.application.routes.url_helpers.account_path)
component.with_navigation_item(text: "Sign out", href: Rails.application.routes.url_helpers.destroy_user_session_path)
end %>
) %>
<%= govuk_service_navigation(
service_name: t("service_name"),
service_url: Rails.application.routes.url_helpers.root_path,
classes: "app-main-service-navigation app-service-navigation--orange",
) do |component| %>
<%= component.with_navigation_item(text: "Your account", href: Rails.application.routes.url_helpers.account_path) %>
<%= component.with_navigation_item(text: "Sign out", href: Rails.application.routes.url_helpers.destroy_user_session_path) %>
<% end %>

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

@ -36,7 +36,7 @@
<% if user.is_data_protection_officer? %>
<%= govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
colour: "teal",
text: "Data protection officer",
) %>
<% else %>
@ -45,7 +45,7 @@
<% if user.is_key_contact? %>
<%= govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
colour: "teal",
text: "Key contact",
) %>
<% else %>

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

@ -54,7 +54,7 @@
options: { disabled: [""], selected: @organisation_id ? answer_options.first : "" } %>
<% end %>
<% hints_for_roles = { data_provider: ["Can view and submit logs for this organisation"], data_coordinator: ["Can view and submit logs for this organisation and any of its managing agents", "Can manage details for this organisation", "Can manage users for this organisation"], support: nil } %>
<% hints_for_roles = { data_provider: ["Can view and submit logs for this organisation"], data_coordinator: ["Can view and submit logs for this organisation and any of its managing agents", "Can manage details for this organisation", "Can manage users for this organisation"], support: ["Can only be created for the MHCLG organisation in the CORE service, to be used by MHCLG and its contractor staff", "Has access to all organisations' data across the CORE service", "Cannot be created for users in housing organisations as this would be a data protection breach"] } %>
<% roles_with_hints = current_user.assignable_roles.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize, description: hints_for_roles[key.to_sym]) } %>

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

4
config/locales/en.yml

@ -260,6 +260,10 @@ en:
blank: "Enter an email address."
role:
invalid: "Role must be data accessor, data provider or data coordinator."
user:
support_user_in_wrong_organisation:
change_role: "You cannot create a support account type for a user in this organisation. Support accounts should only be created for MHCLG and contractor staff as they are administrator level accounts with access to all organisations' data. Any support accounts for housing organisations would be a data protection breach."
change_organisation: "You cannot move a user with a support account to a non-MHCLG organisation. If you need to move the user, change their role type to data coordinator or data provider."
setup:
saledate:

2
config/locales/forms/2024/sales/sale_information.en.yml

@ -57,7 +57,7 @@ en:
check_answer_label: "Part of a back-to-back staircasing transaction"
check_answer_prompt: "Tell us if this is part of a back-to-back staircasing transaction"
hint_text: ""
question_text: "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
question_text: "Was this part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
resale:
page_header: ""

2
config/locales/forms/2025/lettings/household_characteristics.en.yml

@ -7,7 +7,7 @@ en:
page_header: ""
check_answer_label: "Number of household members"
check_answer_prompt: "Enter total number of household members"
hint_text: "You can provide details for a maximum of 8 people."
hint_text: "You can answer up to 15 people. You will be asked to add details for a maximum of 8 people in the next questions."
question_text: "How many people live in the household for this letting?"
age1:

2
config/locales/forms/2025/lettings/household_situation.en.yml

@ -23,7 +23,7 @@ en:
reason:
check_answer_label: "Reason for leaving last settled home"
check_answer_prompt: ""
hint_text: "You told us this letting is a renewal. We have removed some options because of this."
hint_text: "The tenant’s ‘last settled home’ is their last long-standing home. For tenants who were in temporary accommodation, sleeping rough or otherwise homeless, their last settled home is where they were living previously.<br><br>You told us this letting is a renewal. We have removed some options because of this."
question_text: "What is the tenant’s main reason for the household leaving their last settled home?"
reasonother:
check_answer_label: ""

2
config/locales/forms/2025/sales/sale_information.en.yml

@ -53,7 +53,7 @@ en:
check_answer_label: "Part of a back-to-back staircasing transaction"
check_answer_prompt: "Tell us if this is part of a back-to-back staircasing transaction"
hint_text: "Back-to-back staircasing transactions are used as a way for shared owners who own less than 100% of their property to sell on the open market. It involves the shared owner purchasing the remaining share from their landlord and immediately selling 100% of the property to a buyer on the open market. The landlord is then reimbursed for the staircasing transaction through the proceeds of sale to the buyer."
question_text: "Is this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
question_text: "Was this part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?"
firststair:
page_header: ""

2
config/locales/forms/2026/lettings/household_characteristics.en.yml

@ -7,7 +7,7 @@ en:
page_header: ""
check_answer_label: "Number of household members"
check_answer_prompt: "Enter total number of household members"
hint_text: "You can provide details for a maximum of 8 people."
hint_text: "You can answer up to 15 people. You will be asked to add details for a maximum of 8 people in the next questions."
question_text: "How many people live in the household for this letting?"
age1:

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

Loading…
Cancel
Save