Browse Source

Merge branch 'main' into CLDC-3499-remove-pre-2025-tests

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

79
.claude/settings.json

@ -0,0 +1,79 @@
{
"enabledPlugins": {
"dependency-track@skillwire": true
},
"permissions": {
"allow": [
"WebSearch",
"Bash(rg:*)",
"Bash(ls:*)",
"Bash(bundle exec rspec:*)",
"Bash(bundle exec rake lint)",
"Bash(bundle exec rake parallel:spec)",
"Bash(bundle exec rake parallel:setup)",
"Bash(bundle exec rubocop:*)",
"Bash(bundle exec erb_lint:*)",
"Bash(bundle exec rails:*)",
"Bash(bundle exec rake db:migrate)",
"Bash(bundle exec rake db:migrate:status)",
"Bash(bundle install)",
"Bash(yarn install)",
"Bash(yarn standard:*)",
"Bash(yarn prettier:*)",
"Bash(yarn stylelint:*)",
"Bash(yarn build:*)",
"Bash(git fetch)",
"Bash(git pull)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git branch:*)",
"Bash(git checkout:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Edit(app/**)",
"Edit(config/**)",
"Edit(db/migrate/**)",
"Edit(lib/**)",
"Edit(spec/**)",
"Edit(docs/**)"
],
"deny": [
"Read(.env*)",
"Read(config/master.key)",
"Edit(.env*)",
"Edit(config/master.key)",
"Edit(config/credentials.yml.enc)",
"Edit(db/schema.rb)",
"Edit(db/structure.sql)",
"Edit(Gemfile.lock)",
"Edit(yarn.lock)",
"Edit(AGENTS.md)",
"Edit(AI_CONTEXT.md)",
"Edit(WORKFLOW_AI.md)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(sudo:*)",
"Bash(rm -rf:*)",
"Bash(chmod:*)",
"Bash(chown:*)",
"Bash(echo:*)",
"Bash(cat:*)",
"Bash(tail:*)",
"Bash(head:*)",
"Bash(gh:*)",
"Bash(git add .)",
"Bash(git add --all)",
"Bash(git add -A)",
"Bash(git push --force)",
"Bash(git push --force-with-lease)",
"Bash(git reset --hard)",
"Bash(bundle exec rake db:drop)",
"Bash(bundle exec rake db:reset)"
]
},
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}

11
.github/workflows/production_pipeline.yml

@ -20,3 +20,14 @@ jobs:
release_tag: ${{ needs.test.outputs.releasetag }}
permissions:
id-token: write
sbom:
name: Upload SBOM
needs: [aws_deploy]
permissions:
contents: read
uses: ./.github/workflows/upload-sbom.yml
with:
projectversion: prod
secrets:
DTRACK_API_KEY: ${{ secrets.DTRACK_API_KEY }}

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

9
.github/workflows/staging_pipeline.yml

@ -32,6 +32,15 @@ jobs:
permissions:
id-token: write
sbom:
name: Upload SBOM
needs: [aws_deploy]
uses: ./.github/workflows/upload-sbom.yml
with:
projectversion: staging
secrets:
DTRACK_API_KEY: ${{ secrets.DTRACK_API_KEY }}
performance:
needs: [aws_deploy]
runs-on: ubuntu-latest

55
.github/workflows/upload-sbom.yml

@ -0,0 +1,55 @@
name: Upload SBOM
# Generates a CycloneDX SBOM with Syft (auto-detects both the Ruby gems in
# Gemfile.lock and the Node packages in yarn.lock) and uploads it to
# Dependency-Track. Called from the staging and production pipelines after a
# successful deploy.
on:
workflow_call:
inputs:
projectversion:
required: true
type: string
secrets:
DTRACK_API_KEY:
required: true
permissions:
contents: read
jobs:
sbom:
name: Generate and upload SBOM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
echo "SYFT_VERSION=v1.45.0" >> "$GITHUB_ENV"
echo "SYFT_SCRIPT_SHA=9673f867e50398b5d25ec97ff051a451c46d262c" >> "$GITHUB_ENV"
- uses: actions/cache@v4
with:
path: /usr/local/bin/syft
key: syft-${{ env.SYFT_VERSION }}
- name: Install Syft
run: |
[ -f /usr/local/bin/syft ] || \
curl -sSfL "https://raw.githubusercontent.com/anchore/syft/${SYFT_SCRIPT_SHA}/install.sh" | sh -s -- -b /usr/local/bin "$SYFT_VERSION"
- run: syft . -o cyclonedx-xml=bom.xml
- name: Upload SBOM to Dependency-Track
env:
DTRACK_API_KEY: ${{ secrets.DTRACK_API_KEY }}
SBOM_VERSION: ${{ inputs.projectversion }}
run: |
curl -sSf -X POST "https://api-deps.softwire.com/api/v1/bom" \
-H "X-Api-Key: $DTRACK_API_KEY" \
-F "autoCreate=true" \
-F "projectName=CORE" \
-F "projectVersion=$SBOM_VERSION" \
-F "parentName=Support" \
-F "bom=@bom.xml"

2
.nvmrc

@ -1 +1 @@
20
24

2
.ruby-version

@ -1 +1 @@
3.4.4
3.4.9

105
CLAUDE.md

@ -0,0 +1,105 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
CORE — a Ruby on Rails app (the "Submit social housing lettings and sales data" service) that collects lettings and sales of social housing data in England for MHCLG. Data providers (Local Authorities and Private Registered Providers) submit logs; the data is exported nightly to CDS (Consolidated Data Store) via XML to S3.
Stack: Ruby 3.4.9, Rails 7.2, PostgreSQL, Sidekiq + Redis, Webpack/Propshaft, Stimulus, ViewComponent, GOV.UK Design System (govuk-frontend, govuk-components, govuk_design_system_formbuilder), Devise + 2FA, Pundit, PaperTrail.
Full domain/architecture docs live in `docs/` (rendered at https://communitiesuk.github.io/submit-social-housing-lettings-and-sales-data) and are the authoritative reference — especially `docs/index.md` (domain overview), `docs/form/*` (form architecture), `docs/bulk_upload.md`, `docs/exports.md`, `docs/csv_downloads.md`, and the ADRs in `docs/adr/`.
## Commands
Run app (Rails + Sidekiq's redis + JS watch via Foreman):
```bash
./bin/dev
```
Rails server alone: `bundle exec rails s` (port 3000). JS watch alone: `yarn build --mode=development --watch`. First-time asset build: `yarn build --mode=development`.
Tests:
```bash
bundle exec rspec # full suite
bundle exec rspec ./spec/path/to/file.rb # single file
bundle exec rspec ./spec/path/to/file.rb:42 # single example by line
bundle exec rake parallel:setup # one-time setup
RAILS_ENV=test bundle exec rake parallel:spec # parallel run
```
If you change the schema, run `bundle exec rake db:migrate RAILS_ENV=test` before running specs.
Lint (everything): `bundle exec rake lint`. Individual linters:
```bash
bundle exec rubocop # -a safe autocorrect, -A all
bundle exec erb_lint --lint-all
yarn standard # --fix to autocorrect
yarn stylelint app/frontend/styles
yarn prettier . --check # --write to autocorrect
```
Database: `bundle exec rake db:create db:migrate db:seed`. Seeded users use the password from `REVIEW_APP_USER_PASSWORD` in `.env` (default `password`).
## Architecture
### The form system (core abstraction)
Form data collection runs on annual windows (1 April → 1 April + 3-month late-submission tail). Two forms may be active simultaneously during the April–June/July **crossover period**.
`FormHandler` (singleton, `app/models/form_handler.rb`) holds every active form: `current/previous/next/archived` × `lettings/sales`. Each `Form` is built from Ruby classes (not JSON — historical JSON definitions in `config/forms/` are legacy; new forms are defined in code under `app/models/form/lettings/{sections,subsections,pages,questions}` and `app/models/form/sales/...`).
Hierarchy: `Form``Section``Subsection``Page``Question`. Pages route via `depends_on` conditions (with chained method calls e.g. `{ "owning_organisation.provider_type": "local_authority" }`) or custom `routed_to?` methods. Questions can be `conditional_for` (inline conditional on the same page), `derived` (computed, not cleared when unrouted), or `inferred` (cleared when their source changes). See `docs/form/builder.md` for the full DSL.
Key consequence: **every question id must match an ActiveRecord column on `LettingsLog` / `SalesLog`**. Checkbox questions need one column per answer option. Adding a question is a migration + form class change in lock-step.
`Form::DEADLINES` in `app/models/form.rb` is the source of truth for collection year cutoffs (new_logs_end_date, submission_deadline, edit_end_date). Add a year here when introducing a new collection window.
### Logs
`LettingsLog` and `SalesLog` (both inherit shared behaviour from `Log`) are the primary records. Sales splits into discounted ownership, shared-ownership initial, and staircasing (post-2024); pre-2025 also included outright sales. Lettings splits into general needs and supported housing (which belongs to a `Scheme` with one or more `Location`s).
Validations live in `app/models/validations/` (lettings) and `app/models/validations/sales/` and are mixed into the log models. Soft validations show interruption pages instead of hard errors.
### Organisations & permissions
Three user roles outside of MHCLG: **data providers**, **data coordinators** (org admins, can also complete logs), plus an optional **data protection officer (DPO)** flag on a user. Internal roles: **support** (full admin) and **statisticians**. Orgs form parent/child stock-owning/managing relationships (many-to-many), and a user's access to a log depends on whether their org owns or manages it. Pundit policies in `app/policies/` enforce this — always check the policy when adding controller actions.
### Bulk upload
Users upload a CSV per log-type per year; the file is saved to S3 and `ProcessBulkUploadJob` runs `BulkUpload::Processor`, which picks year- and type-specific `CsvParser`, `RowParser`, `Validator`, and `LogCreator` classes from `app/services/bulk_upload/`. Outcomes: clean upload, partial upload (requires user approval after error email), or rejected (template/critical errors). See `docs/bulk_upload.md`.
### Exports to CDS
`Exports::ExportService` orchestrates a nightly Sidekiq cron job that writes XML + manifests to S3 for ingestion by the Consolidated Data Store. Year-specific collections (lettings logs) can produce up to three concurrent collections during crossover. Field-level mapping lives in `lettings_log_export_service.rb`, `organisation_export_service.rb`, `user_export_service.rb`, gated by `EXPORT_FIELDS` constants in the matching `*_export_constants.rb` files (with `POST_<YEAR>_EXPORT_FIELDS` for year-gated additions). Partial vs full export semantics are documented in `docs/exports.md`.
### CSV downloads
User-facing CSV downloads are also async via Sidekiq, delivered as S3 presigned URLs by email. Logs CSVs come in **labels** (human-readable) and **codes** (numeric, aligned with bulk upload / CDS) variants. Column selection lives in `lettings_log_attributes` / `sales_log_attributes` / `scheme_attributes` / `location_attributes` and (for users) `User.download_attributes`. Header descriptions live in the `csv_variable_definitions` table and are edited via `/admin`.
### Rake tasks
`lib/tasks/` holds many one-off and operational rake tasks (data corrections, migrations, exports, form-definition dumps). For ad-hoc production runs, tasks are executed as ECS Fargate tasks against `core-$env-ad-hoc` — see `docs/rake.md`.
### Feature toggles
`app/services/feature_toggle.rb` — simple class methods, no external flag service.
### Frontend conventions
- Service-specific components live in `app/components/` (ViewComponent), with `app-*` BEM class names to avoid clashing with the `govuk-*` Design System.
- Stimulus controllers: register in `app/frontend/controllers/index.js` (kebab-case) and define in `app/frontend/controllers/` (underscore_case).
- Webpack bundles JS/CSS via `jsbundling-rails` + `cssbundling-rails`; Propshaft serves the bundled assets. Babel transpiles to ES5 for IE compatibility (polyfills in `app/frontend/application.js`).
## Testing notes
- Prefer request specs over feature specs (faster, still cover route + controller + model + view). Use feature specs only when JavaScript or interaction-specific assertions are needed.
- FactoryBot factories have deep callback chains: creating a `lettings_log`/`sales_log` also creates a `User`, `Organisation`, often a `DataProtectionConfirmation`, and an `OrganisationRentPeriod` (lettings only, if `period` is set). See `docs/testing.md` for the full breakdown — be aware that tests appearing to "just create a log" may be persisting several other records.
- Capybara runs headless by default and uses Gecko driver for `js: true` specs (toggle in `spec/rails_helper.rb`).
## Commit & PR conventions
Commits and PRs are prefixed with the Jira ticket id (e.g. `CLDC-4300: ...`) — see `git log` for examples. Pre-commit hooks (Overcommit) run RuboCop and schema-up-to-date checks; do not skip them.

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

23
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"
gem "rails", "~> 8.1.3"
# 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,11 +40,11 @@ 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.
gem "paper_trail", "~> 15.2"
gem "paper_trail", "~> 17.0"
# Store active record objects in version whodunnits
gem "paper_trail-globalid"
@ -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"
@ -75,9 +75,6 @@ gem "unread"
gem "nokogiri", "~> 1.19.1"
gem "thor", "~> 1.4.0"
# Pinning until activesupport is updated to v8.1.2
gem "connection_pool", "~> 2.5"
group :development, :test do
# Check gems for known vulnerabilities
gem "bundler-audit"
@ -119,7 +116,7 @@ end
gem "cssbundling-rails"
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
gem "excon", "~> 0.111.0"
gem "excon", ">= 1.5.0"
# faraday is a transitive dependency, but we need to force the version to 2.14.1 to avoid a vulnerability.
gem "faraday", ">= 2.14.1"
# faraday is a transitive dependency, but we need to force the version to 2.14.3 to avoid a vulnerability.
gem "faraday", ">= 2.14.3"

261
Gemfile.lock

@ -1,85 +1,86 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.3.1)
actionpack (= 7.2.3.1)
activesupport (= 7.2.3.1)
action_text-trix (2.1.19)
railties
actioncable (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.3.1)
actionpack (= 7.2.3.1)
activejob (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
actionmailbox (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
actionmailer (7.2.3.1)
actionpack (= 7.2.3.1)
actionview (= 7.2.3.1)
activejob (= 7.2.3.1)
activesupport (= 7.2.3.1)
actionmailer (8.1.3.1)
actionpack (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.3.1)
actionview (= 7.2.3.1)
activesupport (= 7.2.3.1)
cgi
actionpack (8.1.3.1)
actionview (= 8.1.3.1)
activesupport (= 8.1.3.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.3)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.3.1)
actionpack (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
actiontext (8.1.3.1)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.3.1)
activesupport (= 7.2.3.1)
actionview (8.1.3.1)
activesupport (= 8.1.3.1)
builder (~> 3.1)
cgi
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.3.1)
activesupport (= 7.2.3.1)
activejob (8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.3.6)
activemodel (7.2.3.1)
activesupport (= 7.2.3.1)
activemodel (8.1.3.1)
activesupport (= 8.1.3.1)
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activerecord (7.2.3.1)
activemodel (= 7.2.3.1)
activesupport (= 7.2.3.1)
activerecord (8.1.3.1)
activemodel (= 8.1.3.1)
activesupport (= 8.1.3.1)
timeout (>= 0.4.0)
activestorage (7.2.3.1)
actionpack (= 7.2.3.1)
activejob (= 7.2.3.1)
activerecord (= 7.2.3.1)
activesupport (= 7.2.3.1)
activestorage (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activesupport (= 8.1.3.1)
marcel (~> 1.0)
activesupport (7.2.3.1)
activesupport (8.1.3.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1, < 6)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
uri (>= 0.13.1)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
auto_strip_attributes (2.6.0)
activerecord (>= 4.0)
@ -115,7 +116,6 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
base64 (0.3.0)
bcrypt (3.1.22)
benchmark (0.5.0)
better_html (2.2.0)
actionview (>= 7.0)
activesupport (>= 7.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)
@ -149,24 +149,26 @@ GEM
capybara-screenshot (1.0.26)
capybara (>= 1.0, < 4)
launchy
cgi (0.5.1)
childprocess (5.0.0)
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.3.6)
connection_pool (2.5.3)
concurrent-ruby (1.3.8)
connection_pool (2.5.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
crass (1.0.7)
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 +189,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,10 +198,11 @@ 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)
excon (1.5.0)
logger
factory_bot (6.5.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.4)
@ -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.3)
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.4.0)
activesupport (>= 6.1)
govuk-components (5.7.0)
govuk-components (6.4.0)
html-attributes-utils (~> 1.0.0, >= 1.0.0)
pagy (>= 6, < 10)
view_component (>= 3.9, < 3.17)
view_component (>= 4.9, < 4.13)
govuk_design_system_formbuilder (5.7.1)
actionview (>= 6.1)
activemodel (>= 6.1)
@ -236,12 +239,12 @@ GEM
hashdiff (1.1.0)
html-attributes-utils (1.0.2)
activesupport (>= 6.1.4.4)
i18n (1.14.8)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
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.20.0)
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)
@ -274,7 +277,7 @@ GEM
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
loofah (2.25.1)
loofah (2.25.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -282,17 +285,17 @@ GEM
net-imap
net-pop
net-smtp
marcel (1.0.4)
marcel (1.2.1)
matrix (0.4.2)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.27.0)
msgpack (1.7.2)
msgpack (1.8.3)
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.1)
date
net-protocol
net-pop (0.1.2)
@ -301,25 +304,25 @@ 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.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-darwin)
nokogiri (1.19.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu)
nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl)
nokogiri (1.19.4-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)
paper_trail (15.2.0)
activerecord (>= 6.1)
pagy (9.4.0)
paper_trail (17.0.0)
activerecord (>= 7.1)
request_store (~> 1.4)
paper_trail-globalid (0.2.0)
globalid
@ -350,45 +353,45 @@ 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)
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (7.2.3.1)
actioncable (= 7.2.3.1)
actionmailbox (= 7.2.3.1)
actionmailer (= 7.2.3.1)
actionpack (= 7.2.3.1)
actiontext (= 7.2.3.1)
actionview (= 7.2.3.1)
activejob (= 7.2.3.1)
activemodel (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
rails (8.1.3.1)
actioncable (= 8.1.3.1)
actionmailbox (= 8.1.3.1)
actionmailer (= 8.1.3.1)
actionpack (= 8.1.3.1)
actiontext (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activemodel (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
bundler (>= 1.15.0)
railties (= 7.2.3.1)
railties (= 8.1.3.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
rails-html-sanitizer (1.7.1)
loofah (~> 2.25, >= 2.25.2)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails_admin (3.3.0)
activemodel-serializers-xml (>= 1.0)
@ -397,10 +400,9 @@ GEM
nested_form (~> 0.3)
rails (>= 6.0, < 9)
turbo-rails (>= 1.0, < 3)
railties (7.2.3.1)
actionpack (= 7.2.3.1)
activesupport (= 7.2.3.1)
cgi
railties (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@ -408,7 +410,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 +421,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)
@ -514,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)
@ -531,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)
@ -539,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.12.0)
actionview (>= 7.1.0)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)
virtus (2.0.0)
axiom-types (~> 0.1)
coercible (~> 1.0)
@ -566,13 +570,13 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.10)
websocket-driver (0.7.7)
websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.5)
zeitwerk (2.8.2)
PLATFORMS
arm64-darwin
@ -590,17 +594,16 @@ DEPENDENCIES
capybara
capybara-lockstep
capybara-screenshot
connection_pool (~> 2.5)
cssbundling-rails
devise
devise_two_factor_authentication
dotenv-rails
erb_lint
excon (~> 0.111.0)
excon (>= 1.5.0)
factory_bot_rails
faker
faraday (>= 2.14.1)
govuk-components (~> 5.7)
faraday (>= 2.14.3)
govuk-components (~> 6.2)
govuk_design_system_formbuilder (~> 5.7)
govuk_markdown
jsbundling-rails
@ -610,19 +613,19 @@ DEPENDENCIES
nokogiri (~> 1.19.1)
notifications-ruby-client
overcommit (>= 0.37.0)
paper_trail (~> 15.2)
paper_trail (~> 17.0)
paper_trail-globalid
parallel_tests
pg (~> 1.1)
possessive
propshaft
pry-byebug
puma (~> 6.4)
puma (~> 7.2.1)
pundit
rack (~> 3.1.20)
rack-attack
rack-mini-profiler (~> 3.3.0)
rails (~> 7.2.2)
rails (~> 8.1.3)
rails_admin (~> 3.1)
redcarpet (~> 3.6)
redis (~> 4.8)
@ -635,7 +638,7 @@ DEPENDENCIES
selenium-webdriver (~> 4.43)
sentry-rails
sentry-ruby
sidekiq
sidekiq (~> 7.2.4)
sidekiq-cron
simplecov
stimulus-rails
@ -644,12 +647,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

1
app/controllers/form_controller.rb

@ -25,6 +25,7 @@ class FormController < ApplicationController
updated_question = @page.questions.reject { |question| question.check_answer_label.blank? }.first
updated_question_string = [updated_question&.question_number_string, updated_question&.check_answer_label.to_s.downcase].compact.join(": ")
flash[:notice] = "You have successfully updated #{updated_question_string}"
flash[:notification_banner_two_thirds] = true
end
update_duplication_tracking

1
app/controllers/lettings_logs_controller.rb

@ -96,6 +96,7 @@ class LettingsLogsController < LogsController
end
def email_csv
DownloadRecord.build_from_user(download_type: :lettings_log, download_filters: session_filters.to_s, user: current_user).save!
all_orgs = params["organisation_select"] == "all"
EmailCsvJob.perform_later(current_user, search_term, session_filters, all_orgs, nil, codes_only_export?, "lettings", session_filters["years"].first.to_i)
redirect_to csv_confirmation_lettings_logs_path

4
app/controllers/organisations_controller.rb

@ -40,6 +40,7 @@ class OrganisationsController < ApplicationController
end
def email_schemes_csv
DownloadRecord.build_from_user(download_type: :scheme_location, download_filters: session_filters.merge({ download_type: params[:download_type] }).to_s, user: current_user).save!
SchemeEmailCsvJob.perform_later(current_user, search_term, session_filters, false, @organisation, params[:download_type])
redirect_to schemes_csv_confirmation_organisation_path
end
@ -72,6 +73,7 @@ class OrganisationsController < ApplicationController
end
end
format.csv do
DownloadRecord.build_from_user(download_type: :user, download_filters: session_filters.to_s, user: current_user).save!
send_data byte_order_mark + unpaginated_filtered_users.to_csv, filename: "users-#{@organisation.name}-#{Time.zone.now}.csv"
end
end
@ -205,6 +207,7 @@ class OrganisationsController < ApplicationController
end
def email_lettings_csv
DownloadRecord.build_from_user(download_type: :lettings_log, download_filters: session_filters.to_s, user: current_user).save!
EmailCsvJob.perform_later(current_user, search_term, session_filters, false, @organisation, codes_only_export?, "lettings", session_filters["years"].first.to_i)
redirect_to lettings_logs_csv_confirmation_organisation_path
end
@ -243,6 +246,7 @@ class OrganisationsController < ApplicationController
end
def email_sales_csv
DownloadRecord.build_from_user(download_type: :sales_log, download_filters: session_filters.to_s, user: current_user).save!
EmailCsvJob.perform_later(current_user, search_term, session_filters, false, @organisation, codes_only_export?, "sales", session_filters["years"].first.to_i)
redirect_to sales_logs_csv_confirmation_organisation_path
end

1
app/controllers/sales_logs_controller.rb

@ -70,6 +70,7 @@ class SalesLogsController < LogsController
end
def email_csv
DownloadRecord.build_from_user(download_type: :sales_log, download_filters: session_filters.to_s, user: current_user).save!
all_orgs = params["organisation_select"] == "all"
EmailCsvJob.perform_later(current_user, search_term, session_filters, all_orgs, nil, codes_only_export?, "sales", session_filters["years"].first.to_i)
redirect_to csv_confirmation_sales_logs_path

1
app/controllers/schemes_controller.rb

@ -236,6 +236,7 @@ class SchemesController < ApplicationController
end
def email_csv
DownloadRecord.build_from_user(download_type: :scheme_location, download_filters: session_filters.merge({ download_type: params[:download_type] }).to_s, user: current_user).save!
all_orgs = params["organisation_select"] == "all"
SchemeEmailCsvJob.perform_later(current_user, search_term, session_filters, all_orgs, nil, params[:download_type])
redirect_to csv_confirmation_schemes_path

13
app/controllers/users_controller.rb

@ -11,7 +11,7 @@ class UsersController < ApplicationController
before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index]
def index
redirect_to users_organisation_path(current_user.organisation) unless current_user.support?
return redirect_to users_organisation_path(current_user.organisation) unless current_user.support? || request.format.csv?
all_users = User.visible.sorted_by_organisation_and_role
filtered_users = filter_manager.filtered_users(all_users, search_term, session_filters)
@ -24,6 +24,7 @@ class UsersController < ApplicationController
format.html
format.csv do
if current_user.support?
DownloadRecord.build_from_user(download_type: :user, download_filters: session_filters.to_s, user: current_user).save!
send_data byte_order_mark + filtered_users.to_csv, filename: "users-#{Time.zone.now}.csv"
else
head :unauthorized
@ -221,8 +222,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/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;
}

28
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)
@ -32,6 +29,21 @@ module ApplicationHelper
!request.path.match?(/\/notifications\/\d+$/) && (authenticated_user_has_notifications? || unauthenticated_user_has_notifications?)
end
def notification_banner
govuk_notification_banner(
title_text: "Success",
success: true,
title_heading_level: 3,
title_id: "flash-notice",
role: "alert",
) do |notification_banner|
notification_banner.with_heading(text: flash.notice.html_safe)
if flash[:notification_banner_body]
tag.p flash[:notification_banner_body]&.html_safe
end
end
end
private
def paginated_title(title, pagy)

6
app/helpers/collection_time_helper.rb

@ -106,6 +106,10 @@ module CollectionTimeHelper
# useful for writing future tests that will also test the current time if it can or a future year if needed.
# stops tests being frozen on a specific year.
def collection_start_date_for_year_or_later(year)
collection_start_date_for_year([current_collection_start_year, year].max)
collection_start_date_for_year(collection_start_year_for_year_or_later(year))
end
def collection_start_year_for_year_or_later(year)
[current_collection_start_year, year].max
end
end

2
app/helpers/user_helper.rb

@ -91,7 +91,7 @@ module UserHelper
end
def user_action_text(user, attribute)
return "Change" if %w[role phone].include?(attribute) && user.send(attribute).present?
return "Change" if attribute == "phone_extension" || (%w[role phone].include?(attribute) && user.send(attribute).present?)
""
end

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/download_record.rb

@ -0,0 +1,25 @@
# Used to allow for easier auditing of what users downloaded what info
# Caches some info about the user at the time of download
class DownloadRecord < ApplicationRecord
belongs_to :user
belongs_to :user_organisation, class_name: "Organisation"
DOWNLOAD_TYPE = {
user: 0,
lettings_log: 1,
sales_log: 2,
scheme_location: 3,
}.freeze
enum :download_type, DOWNLOAD_TYPE
enum :user_role, User::ROLES
def self.build_from_user(user:, **attrs)
new(
user:,
user_organisation: user.organisation,
user_role: user.role,
**attrs,
)
end
end

4
app/models/form.rb

@ -21,8 +21,8 @@ class Form
},
2025 => {
submission_deadline: Time.zone.local(2026, 6, 5),
new_logs_end_date: Time.zone.local(2026, 7, 3),
edit_end_date: Time.zone.local(2026, 8, 1),
new_logs_end_date: Time.zone.local(2026, 7, 12),
edit_end_date: Time.zone.local(2026, 9, 1),
},
2026 => {
submission_deadline: Time.zone.local(2027, 6, 4),

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/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 = "£"

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?

2
app/models/location.rb

@ -172,7 +172,7 @@ class Location < ApplicationRecord
LOCAL_AUTHORITIES = LocalAuthority.all.map { |la| [la.name, la.code] }.to_h
attribute :local_authorities, :string
enum :local_authorities, LOCAL_AUTHORITIES
enum :local_authorities, LOCAL_AUTHORITIES if LOCAL_AUTHORITIES.present?
def self.local_authorities_for_current_year
LocalAuthority.all.active(Time.zone.today).england.map { |la| [la.code, la.name] }.to_h
end

9
app/models/log.rb

@ -50,7 +50,6 @@ class Log < ApplicationRecord
scope :has_old_form_id, -> { where.not(old_form_id: nil) }
scope :imported_2023_with_old_form_id, -> { imported.filter_by_year(2023).has_old_form_id }
scope :imported_2023, -> { imported.filter_by_year(2023) }
# TODO: CLDC-4273: use .union in filter_by_organisation rather than raw SQL
scope :filter_by_organisation, lambda { |orgs, _user = nil|
owned = unscoped { where(owning_organisation: orgs).select(:id) }
managed = unscoped { where(managing_organisation: orgs).select(:id) }
@ -204,6 +203,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

25
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,
@ -334,12 +335,6 @@ class User < ApplicationRecord
save!(validate: false)
end
def phone_with_extension
return phone if phone_extension.blank?
"#{phone}, Ext. #{phone_extension}"
end
def assigned_to_lettings_logs
lettings_logs.where(assigned_to: self)
end
@ -390,6 +385,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 +408,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?

1
app/policies/user_policy.rb

@ -26,6 +26,7 @@ class UserPolicy
%w[
edit_emails?
edit_telephone_numbers?
edit_extension_numbers?
edit_names?
].each do |method_name|
define_method method_name do

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

39
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
@ -763,7 +781,7 @@ private
postcode_full: %i[field_21 field_22],
wchair: %i[field_27],
type: %i[field_9 field_11 field_8],
type: sale_type_fields,
resale: %i[field_78],
hodate: %i[field_80 field_81 field_82],
@ -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
@ -1191,6 +1209,13 @@ private
field_109 if staircasing?
end
def sale_type_fields
return %i[field_8 field_9] if shared_ownership?
return %i[field_8 field_11] if discounted_ownership?
%i[field_9 field_11 field_8]
end
def value_fields
return [:field_86] if shared_ownership_initial_purchase?
return [:field_113] if discounted_ownership?
@ -1283,7 +1308,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 +1526,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

38
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
@ -817,7 +836,7 @@ private
postcode_full: %i[field_21 field_22],
wchair: %i[field_28],
type: %i[field_9 field_11 field_8],
type: sale_type_fields,
resale: %i[field_91],
hodate: %i[field_93 field_94 field_95],
@ -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
@ -1323,6 +1342,13 @@ private
field_122 if staircasing?
end
def sale_type_fields
return %i[field_8 field_9] if shared_ownership?
return %i[field_8 field_11] if discounted_ownership?
%i[field_9 field_11 field_8]
end
def value_fields
return [:field_99] if shared_ownership_initial_purchase?
return [:field_128] if discounted_ownership?
@ -1416,7 +1442,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 +1680,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

1
app/services/exports/user_export_constants.rb

@ -6,6 +6,7 @@ module Exports::UserExportConstants
"email",
"name",
"phone",
"extension_number",
"organisation_id",
"organisation_name",
"role",

3
app/services/exports/user_export_service.rb

@ -64,7 +64,8 @@ module Exports
attribute_hash["role"] = user.role
attribute_hash["organisation_name"] = user.organisation.name
attribute_hash["active"] = user.active?
attribute_hash["phone"] = [user.phone, user.phone_extension].compact.join(" ")
attribute_hash["phone"] = user.phone
attribute_hash["extension_number"] = user.phone_extension
attribute_hash["last_sign_in_at"] = user.last_sign_in_at&.iso8601
attribute_hash
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>

78
app/views/layouts/_footer.html.erb

@ -1,42 +1,44 @@
<%= govuk_footer do |footer| %>
<%= footer.with_meta do %>
<div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
<h2 class="govuk-heading-m">Get help with this service</h2>
<div class="govuk-grid-row">
<div class="govuk-grid-column-one-third-from-desktop">
<h3 class="govuk-heading-s govuk-!-margin-bottom-1">Online helpdesk</h3>
<p class="govuk-body govuk-!-font-size-16">
<%= govuk_link_to("CORE helpdesk (opens in a new tab)", GlobalConstants::HELPDESK_URL, class: "govuk-footer__link", rel: "noreferrer noopener", target: "_blank") %>
</p>
</div>
<footer class="govuk-footer" role="contentinfo">
<div class="govuk-width-container">
<div class="govuk-footer__meta">
<div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
<h2 class="govuk-heading-m">Get help with this service</h2>
<div class="govuk-grid-row">
<div class="govuk-grid-column-one-third-from-desktop">
<h3 class="govuk-heading-s govuk-!-margin-bottom-1">Online helpdesk</h3>
<p class="govuk-body govuk-!-font-size-16">
<%= govuk_link_to("CORE helpdesk (opens in a new tab)", GlobalConstants::HELPDESK_URL, class: "govuk-footer__link", rel: "noreferrer noopener", target: "_blank") %>
</p>
</div>
<div class="govuk-grid-column-one-third-from-desktop">
<h3 class="govuk-heading-s govuk-!-margin-bottom-1">Telephone</h3>
<ul class="govuk-list govuk-!-font-size-16">
<li>0333 202 5084</li>
<li>Monday to Friday, 9am to 5:30pm<br>(except public holidays)</li>
</ul>
<div class="govuk-grid-column-one-third-from-desktop">
<h3 class="govuk-heading-s govuk-!-margin-bottom-1">Telephone</h3>
<ul class="govuk-list govuk-!-font-size-16">
<li>0333 202 5084</li>
<li>Monday to Friday, 9am to 5:30pm<br>(except public holidays)</li>
</ul>
</div>
</div>
</div>
<h2 class="govuk-visually-hidden">Helpful links</h2>
<ul class="govuk-footer__inline-list govuk-!-margin-top-6 govuk-!-margin-bottom-0">
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("Accessibility statement", accessibility_statement_path, class: "govuk-footer__link") %>
</li>
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("Privacy notice", privacy_notice_path, class: "govuk-footer__link") %>
</li>
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("View source code", "https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data", class: "govuk-footer__link") %>
</li>
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("Cookies", cookies_path, class: "govuk-footer__link") %>
</li>
</ul>
</div>
<div class="govuk-footer__meta-item">
<a class="govuk-footer__link govuk-footer__copyright-logo govuk-!-margin-bottom-1" href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">© Crown copyright</a>
<h2 class="govuk-visually-hidden">Helpful links</h2>
<ul class="govuk-footer__inline-list govuk-!-margin-top-6 govuk-!-margin-bottom-0">
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("Accessibility statement", accessibility_statement_path, class: "govuk-footer__link") %>
</li>
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("Privacy notice", privacy_notice_path, class: "govuk-footer__link") %>
</li>
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("View source code", "https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data", class: "govuk-footer__link") %>
</li>
<li class="govuk-footer__inline-list-item">
<%= govuk_link_to("Cookies", cookies_path, class: "govuk-footer__link") %>
</li>
</ul>
</div>
<div class="govuk-footer__meta-item">
<a class="govuk-footer__link govuk-footer__copyright-logo govuk-!-margin-bottom-1" href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">© Crown copyright</a>
</div>
</div>
<% end %>
<% end %>
</div>
</footer>

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

Loading…
Cancel
Save