Browse Source

Merge branch 'main' into CLDC-3740-Replace-you-didnt-answer-with-link

# Conflicts:
#	app/helpers/schemes_helper.rb
#	config/locales/forms/2023/lettings/household_characteristics.en.yml
#	config/locales/forms/2023/lettings/income_and_benefits.en.yml
#	config/locales/forms/2023/sales/household_characteristics.en.yml
#	config/locales/forms/2023/sales/income_benefits_and_savings.en.yml
#	config/locales/forms/2023/sales/sale_information.en.yml
#	config/locales/forms/2024/lettings/household_characteristics.en.yml
#	config/locales/forms/2024/lettings/income_and_benefits.en.yml
#	config/locales/forms/2024/sales/household_characteristics.en.yml
#	config/locales/forms/2024/sales/sale_information.en.yml
#	config/locales/forms/2025/lettings/household_characteristics.en.yml
#	config/locales/forms/2025/lettings/income_and_benefits.en.yml
#	config/locales/forms/2025/sales/sale_information.en.yml
#	config/locales/forms/2025/sales/setup.en.yml
pull/2836/head
Manny Dinssa 7 months ago
parent
commit
0fe9ee28c0
  1. 22
      .github/workflows/aws_deploy.yml
  2. 6
      .github/workflows/review_teardown_pipeline.yml
  3. 411
      .github/workflows/run_tests.yml
  4. 351
      .github/workflows/staging_pipeline.yml
  5. 2
      Gemfile
  6. 130
      Gemfile.lock
  7. 2
      app/components/search_result_caption_component.html.erb
  8. 23
      app/controllers/bulk_upload_lettings_logs_controller.rb
  9. 19
      app/controllers/bulk_upload_sales_logs_controller.rb
  10. 1
      app/controllers/organisations_controller.rb
  11. 4
      app/controllers/schemes_controller.rb
  12. 18
      app/helpers/schemes_helper.rb
  13. 21
      app/mailers/bulk_upload_mailer.rb
  14. 9
      app/models/derived_variables/lettings_log_variables.rb
  15. 9
      app/models/derived_variables/sales_log_variables.rb
  16. 3
      app/models/export.rb
  17. 6
      app/models/form/sales/questions/buyer2_working_situation.rb
  18. 2
      app/models/form/sales/questions/equity.rb
  19. 2
      app/models/form/sales/questions/value.rb
  20. 39
      app/models/forms/bulk_upload_lettings/needstype.rb
  21. 4
      app/models/log.rb
  22. 1
      app/models/organisation.rb
  23. 2
      app/models/scheme.rb
  24. 8
      app/models/validations/sales/sale_information_validations.rb
  25. 14
      app/services/bulk_upload/lettings/validator.rb
  26. 20
      app/services/bulk_upload/lettings/year2024/row_parser.rb
  27. 40
      app/services/bulk_upload/processor.rb
  28. 12
      app/services/bulk_upload/sales/validator.rb
  29. 20
      app/services/bulk_upload/sales/year2024/row_parser.rb
  30. 10
      app/services/exports/export_service.rb
  31. 22
      app/services/exports/lettings_log_export_service.rb
  32. 12
      app/services/exports/organisation_export_service.rb
  33. 12
      app/services/exports/user_export_service.rb
  34. 20
      app/services/exports/xml_export_service.rb
  35. 23
      app/views/bulk_upload_lettings_logs/forms/needstype.erb
  36. 2
      app/views/bulk_upload_shared/uploads.html.erb
  37. 2
      app/views/locations/index.html.erb
  38. 2
      app/views/logs/_log_list.html.erb
  39. 2
      app/views/logs/index.html.erb
  40. 2
      app/views/logs/update_logs.html.erb
  41. 4
      app/views/organisation_relationships/managing_agents.html.erb
  42. 4
      app/views/organisation_relationships/stock_owners.html.erb
  43. 2
      app/views/organisations/_organisation_list.html.erb
  44. 2
      app/views/organisations/index.html.erb
  45. 2
      app/views/organisations/logs.html.erb
  46. 2
      app/views/schemes/_scheme_list.html.erb
  47. 2
      app/views/schemes/confirm_secondary.html.erb
  48. 2
      app/views/schemes/details.html.erb
  49. 8
      app/views/schemes/primary_client_group.html.erb
  50. 2
      app/views/schemes/secondary_client_group.html.erb
  51. 2
      app/views/schemes/support.html.erb
  52. 2
      app/views/users/_user_list.html.erb
  53. 1
      config/locales/en.yml
  54. 2
      config/locales/forms/2023/lettings/household_characteristics.en.yml
  55. 40
      config/locales/forms/2023/sales/household_characteristics.en.yml
  56. 8
      config/locales/forms/2023/sales/income_benefits_and_savings.en.yml
  57. 11
      config/locales/forms/2023/sales/sale_information.en.yml
  58. 2
      config/locales/forms/2024/lettings/household_characteristics.en.yml
  59. 40
      config/locales/forms/2024/sales/household_characteristics.en.yml
  60. 2
      config/locales/forms/2024/sales/income_benefits_and_savings.en.yml
  61. 10
      config/locales/forms/2024/sales/sale_information.en.yml
  62. 2
      config/locales/forms/2025/lettings/household_characteristics.en.yml
  63. 20
      config/locales/forms/2025/sales/household_characteristics.en.yml
  64. 2
      config/locales/forms/2025/sales/income_benefits_and_savings.en.yml
  65. 10
      config/locales/forms/2025/sales/sale_information.en.yml
  66. 2
      config/locales/forms/2025/sales/setup.en.yml
  67. 2
      config/locales/validations/lettings/2024/bulk_upload.en.yml
  68. 2
      config/locales/validations/sales/2024/bulk_upload.en.yml
  69. 5
      db/migrate/20241125153349_add_unique_index_to_org_name.rb
  70. 5
      db/migrate/20241204100518_add_year_to_export.rb
  71. 4
      db/schema.rb
  72. 6
      lib/tasks/data_export.rake
  73. 8
      lib/tasks/set_export_collection_years.rake
  74. 26
      spec/components/search_result_caption_component_spec.rb
  75. 7
      spec/db/seeds_spec.rb
  76. 348
      spec/features/lettings_log_spec.rb
  77. 348
      spec/features/sales_log_spec.rb
  78. 4
      spec/fixtures/exports/general_needs_log.xml
  79. 4
      spec/fixtures/exports/general_needs_log_23_24.xml
  80. 4
      spec/fixtures/exports/general_needs_log_24_25.xml
  81. 2
      spec/fixtures/exports/organisation.xml
  82. 4
      spec/fixtures/exports/supported_housing_logs.xml
  83. 2
      spec/fixtures/exports/user.xml
  84. 2
      spec/helpers/tab_nav_helper_spec.rb
  85. 6
      spec/lib/tasks/data_export_spec.rb
  86. 41
      spec/lib/tasks/set_export_collection_years_spec.rb
  87. 25
      spec/mailers/bulk_upload_mailer_spec.rb
  88. 11
      spec/mailers/devise_notify_mailer_spec.rb
  89. 2
      spec/models/form/lettings/questions/managing_organisation_spec.rb
  90. 4
      spec/models/form/sales/pages/equity_spec.rb
  91. 4
      spec/models/form/sales/pages/value_shared_ownership_spec.rb
  92. 28
      spec/models/form/sales/questions/buyer2_working_situation_spec.rb
  93. 4
      spec/models/form/sales/questions/equity_spec.rb
  94. 6
      spec/models/organisation_spec.rb
  95. 7
      spec/models/scheme_spec.rb
  96. 1
      spec/models/user_spec.rb
  97. 111
      spec/requests/bulk_upload_lettings_logs_controller_spec.rb
  98. 111
      spec/requests/bulk_upload_sales_logs_controller_spec.rb
  99. 6
      spec/requests/lettings_logs_controller_spec.rb
  100. 28
      spec/requests/organisation_relationships_controller_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

22
.github/workflows/aws_deploy.yml

@ -41,19 +41,17 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Login to Amazon ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v1
with:
mask-password: "true"
uses: aws-actions/amazon-ecr-login@v2
- name: Check if image with tag already exists
run: |
@ -81,16 +79,14 @@ jobs:
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Login to Amazon ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v1
with:
mask-password: "true"
uses: aws-actions/amazon-ecr-login@v2
- name: Get timestamp
id: timestamp
@ -112,7 +108,7 @@ jobs:
echo "image=$registry/$repository:$readable_tag" >> $GITHUB_ENV
- name: Configure AWS credentials for environment
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: arn:aws:iam::${{ inputs.aws_account_id }}:role/${{ inputs.aws_role_prefix }}-deployment
@ -133,7 +129,7 @@ jobs:
image: ${{ env.image }}
- name: Update ad hoc task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.ad-hoc-task-def.outputs.task-definition }}
@ -185,7 +181,7 @@ jobs:
image: ${{ env.image }}
- name: Deploy updated application
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
cluster: ${{ inputs.aws_task_prefix }}-app
service: ${{ inputs.aws_task_prefix }}-app
@ -207,7 +203,7 @@ jobs:
image: ${{ env.image }}
- name: Deploy updated sidekiq
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
cluster: ${{ inputs.aws_task_prefix }}-app
service: ${{ inputs.aws_task_prefix }}-sidekiq

6
.github/workflows/review_teardown_pipeline.yml

@ -25,13 +25,13 @@ jobs:
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Configure AWS credentials for review environment
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/${{ env.aws_role_prefix }}-deployment
@ -46,7 +46,7 @@ jobs:
network=$(aws ecs describe-services --cluster $cluster --services $service --query services[0].networkConfiguration)
overrides='{ "containerOverrides" : [{ "name" : "app", "command" : ["bundle", "exec", "rake", "db:drop"]}]}'
arn=$(aws ecs run-task --cluster $cluster --task-definition $ad_hoc_task_definition --network-configuration "$network" --overrides "$overrides" --group migrations --launch-type FARGATE --query tasks[0].taskArn)
echo "Waiting for db prepare task to complete"
echo "Waiting for db drop task to complete"
temp=${arn##*/}
id=${temp%*\"}
aws ecs wait tasks-stopped --cluster $cluster --tasks $id

411
.github/workflows/run_tests.yml

@ -0,0 +1,411 @@
name: Run Tests
on:
workflow_call:
pull_request:
types:
- opened
- synchronize
merge_group:
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec\/(?!features|models|requests|services)']
feature_test:
name: Feature Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features --fail-fast --exclude-pattern "spec/features/accessibility_spec.rb"
model_test:
name: Model tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/models --fail-fast
requests_test:
name: Requests tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec/requests']
services_test:
name: Services Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec\/services']
accessibility_test:
name: Accessibility tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features/accessibility_spec.rb --fail-fast
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Install packages and symlink local dependencies
run: |
yarn install --immutable --immutable-cache --check-cache
- name: Lint
run: |
bundle exec rake lint
audit:
name: Audit dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Audit
run: |
bundle exec bundler-audit

351
.github/workflows/staging_pipeline.yml

@ -4,11 +4,6 @@ on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
merge_group:
workflow_dispatch:
defaults:
@ -21,347 +16,13 @@ env:
repository: core
jobs:
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec\/(?!features|models|requests)']
feature_test:
name: Feature Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features --fail-fast --exclude-pattern "spec/features/accessibility_spec.rb"
model_test:
name: Model tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/models --fail-fast
requests_test:
name: Requests tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec/requests']
accessibility_test:
name: Accessibility tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features/accessibility_spec.rb --fail-fast
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Install packages and symlink local dependencies
run: |
yarn install --immutable --immutable-cache --check-cache
- name: Lint
run: |
bundle exec rake lint
audit:
name: Audit dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Audit
run: |
bundle exec bundler-audit
tests:
name: Run Tests
uses: ./.github/workflows/run_tests.yml
aws_deploy:
name: AWS Deploy
if: github.ref == 'refs/heads/main'
needs: [lint, test, feature_test, requests_test, model_test, audit]
needs: [tests]
uses: ./.github/workflows/aws_deploy.yml
with:
aws_account_id: 107155005276
@ -379,13 +40,13 @@ jobs:
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Configure AWS credentials for the environment
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: eu-west-2
role-to-assume: arn:aws:iam::107155005276:role/core-staging-deployment

2
Gemfile

@ -6,7 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.1.4"
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem "rails", "~> 7.0.8.5"
gem "rails", "~> 7.0.8.7"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use Puma as the app server

130
Gemfile.lock

@ -1,71 +1,71 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8.5)
actionpack (= 7.0.8.5)
activesupport (= 7.0.8.5)
actioncable (7.0.8.7)
actionpack (= 7.0.8.7)
activesupport (= 7.0.8.7)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.5)
actionpack (= 7.0.8.5)
activejob (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
actionmailbox (7.0.8.7)
actionpack (= 7.0.8.7)
activejob (= 7.0.8.7)
activerecord (= 7.0.8.7)
activestorage (= 7.0.8.7)
activesupport (= 7.0.8.7)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.5)
actionpack (= 7.0.8.5)
actionview (= 7.0.8.5)
activejob (= 7.0.8.5)
activesupport (= 7.0.8.5)
actionmailer (7.0.8.7)
actionpack (= 7.0.8.7)
actionview (= 7.0.8.7)
activejob (= 7.0.8.7)
activesupport (= 7.0.8.7)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.5)
actionview (= 7.0.8.5)
activesupport (= 7.0.8.5)
actionpack (7.0.8.7)
actionview (= 7.0.8.7)
activesupport (= 7.0.8.7)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.5)
actionpack (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
actiontext (7.0.8.7)
actionpack (= 7.0.8.7)
activerecord (= 7.0.8.7)
activestorage (= 7.0.8.7)
activesupport (= 7.0.8.7)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.5)
activesupport (= 7.0.8.5)
actionview (7.0.8.7)
activesupport (= 7.0.8.7)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.8.5)
activesupport (= 7.0.8.5)
activejob (7.0.8.7)
activesupport (= 7.0.8.7)
globalid (>= 0.3.6)
activemodel (7.0.8.5)
activesupport (= 7.0.8.5)
activemodel (7.0.8.7)
activesupport (= 7.0.8.7)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (7.0.8.5)
activemodel (= 7.0.8.5)
activesupport (= 7.0.8.5)
activestorage (7.0.8.5)
actionpack (= 7.0.8.5)
activejob (= 7.0.8.5)
activerecord (= 7.0.8.5)
activesupport (= 7.0.8.5)
activerecord (7.0.8.7)
activemodel (= 7.0.8.7)
activesupport (= 7.0.8.7)
activestorage (7.0.8.7)
actionpack (= 7.0.8.7)
activejob (= 7.0.8.7)
activerecord (= 7.0.8.7)
activesupport (= 7.0.8.7)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.5)
activesupport (7.0.8.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -149,7 +149,7 @@ GEM
crass (1.0.6)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
date (3.3.4)
date (3.4.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.9.3)
@ -246,7 +246,7 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.22.0)
loofah (2.23.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -258,13 +258,13 @@ GEM
matrix (0.4.2)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.25.1)
minitest (5.25.4)
msgpack (1.7.2)
multipart-post (2.4.1)
nested_form (0.3.2)
net-http (0.4.1)
uri
net-imap (0.4.17)
net-imap (0.5.1)
date
net-protocol
net-pop (0.1.2)
@ -273,12 +273,12 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
nio4r (2.7.3)
nokogiri (1.16.7-arm64-darwin)
nio4r (2.7.4)
nokogiri (1.17.1-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-darwin)
nokogiri (1.17.1-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
nokogiri (1.17.1-x86_64-linux)
racc (~> 1.4)
notifications-ruby-client (6.0.0)
jwt (>= 1.5, < 3)
@ -327,36 +327,36 @@ GEM
rack (>= 1.2.0)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.8.5)
actioncable (= 7.0.8.5)
actionmailbox (= 7.0.8.5)
actionmailer (= 7.0.8.5)
actionpack (= 7.0.8.5)
actiontext (= 7.0.8.5)
actionview (= 7.0.8.5)
activejob (= 7.0.8.5)
activemodel (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
rails (7.0.8.7)
actioncable (= 7.0.8.7)
actionmailbox (= 7.0.8.7)
actionmailer (= 7.0.8.7)
actionpack (= 7.0.8.7)
actiontext (= 7.0.8.7)
actionview (= 7.0.8.7)
activejob (= 7.0.8.7)
activemodel (= 7.0.8.7)
activerecord (= 7.0.8.7)
activestorage (= 7.0.8.7)
activesupport (= 7.0.8.7)
bundler (>= 1.15.0)
railties (= 7.0.8.5)
railties (= 7.0.8.7)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.1)
loofah (~> 2.21)
nokogiri (~> 1.14)
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.1.3)
activemodel-serializers-xml (>= 1.0)
kaminari (>= 0.14, < 2.0)
nested_form (~> 0.3)
rails (>= 6.0, < 8)
turbo-rails (~> 1.0)
railties (7.0.8.5)
actionpack (= 7.0.8.5)
activesupport (= 7.0.8.5)
railties (7.0.8.7)
actionpack (= 7.0.8.7)
activesupport (= 7.0.8.7)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -465,7 +465,7 @@ GEM
thor (1.3.2)
thread_safe (0.3.6)
timecop (0.9.8)
timeout (0.4.1)
timeout (0.4.2)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
@ -553,7 +553,7 @@ DEPENDENCIES
rack (>= 2.2.6.3)
rack-attack
rack-mini-profiler (~> 2.0)
rails (~> 7.0.8.5)
rails (~> 7.0.8.7)
rails_admin (~> 3.1)
redcarpet (~> 3.6)
redis (~> 4.8)

2
app/components/search_result_caption_component.html.erb

@ -7,7 +7,7 @@
<strong><%= count %></strong> <%= item_label.pluralize(count) %> matching filters<br>
<% else %>
<span class="govuk-!-margin-right-4">
<strong><%= count %></strong> total <%= item %>
<strong><%= count %></strong> total <%= item.pluralize(count) %>
</span>
<% end %>
</span>

23
app/controllers/bulk_upload_lettings_logs_controller.rb

@ -1,6 +1,7 @@
class BulkUploadLettingsLogsController < ApplicationController
before_action :authenticate_user!
before_action :validate_data_protection_agrement_signed!
before_action :validate_data_protection_agreement_signed!
before_action :validate_year!, except: %w[start]
def start
if have_choice_of_year?
@ -24,12 +25,26 @@ class BulkUploadLettingsLogsController < ApplicationController
private
def validate_data_protection_agrement_signed!
def validate_data_protection_agreement_signed!
return if @current_user.organisation.data_protection_confirmed?
redirect_to lettings_logs_path
end
def validate_year!
return if params[:id] == "year"
return if params[:id] == "guidance" && params.dig(:form, :year).nil?
allowed_years = [current_year]
allowed_years.push(current_year - 1) if FormHandler.instance.lettings_in_crossover_period?
allowed_years.push(current_year + 1) if FeatureToggle.allow_future_form_use?
provided_year = params.dig(:form, :year)&.to_i
return if allowed_years.include?(provided_year)
render_not_found
end
def current_year
FormHandler.instance.current_collection_start_year
end
@ -48,8 +63,6 @@ private
Forms::BulkUploadLettings::PrepareYourFile.new(form_params)
when "guidance"
Forms::BulkUploadLettings::Guidance.new(form_params.merge(referrer: params[:referrer]))
when "needstype"
Forms::BulkUploadLettings::Needstype.new(form_params)
when "upload-your-file"
Forms::BulkUploadLettings::UploadYourFile.new(form_params.merge(current_user:))
when "checking-file"
@ -60,6 +73,6 @@ private
end
def form_params
params.fetch(:form, {}).permit(:year, :needstype, :file, :organisation_id)
params.fetch(:form, {}).permit(:year, :file, :organisation_id)
end
end

19
app/controllers/bulk_upload_sales_logs_controller.rb

@ -1,6 +1,7 @@
class BulkUploadSalesLogsController < ApplicationController
before_action :authenticate_user!
before_action :validate_data_protection_agrement_signed!
before_action :validate_data_protection_agreement_signed!
before_action :validate_year!, except: %w[start]
def start
if have_choice_of_year?
@ -24,12 +25,26 @@ class BulkUploadSalesLogsController < ApplicationController
private
def validate_data_protection_agrement_signed!
def validate_data_protection_agreement_signed!
return if @current_user.organisation.data_protection_confirmed?
redirect_to sales_logs_path
end
def validate_year!
return if params[:id] == "year"
return if params[:id] == "guidance" && params.dig(:form, :year).nil?
allowed_years = [current_year]
allowed_years.push(current_year - 1) if FormHandler.instance.sales_in_crossover_period?
allowed_years.push(current_year + 1) if FeatureToggle.allow_future_form_use?
provided_year = params.dig(:form, :year)&.to_i
return if allowed_years.include?(provided_year)
render_not_found
end
def current_year
FormHandler.instance.current_collection_start_year
end

1
app/controllers/organisations_controller.rb

@ -155,6 +155,7 @@ class OrganisationsController < ApplicationController
end
redirect_to details_organisation_path(@organisation)
else
@used_rent_periods = @organisation.lettings_logs.pluck(:period).uniq.compact.map(&:to_s)
@rent_periods = helpers.rent_periods_with_checked_attr(checked_periods: selected_rent_periods)
render :edit, status: :unprocessable_entity
end

4
app/controllers/schemes_controller.rb

@ -150,7 +150,7 @@ class SchemesController < ApplicationController
@scheme.update!(secondary_client_group: nil) if @scheme.has_other_client_group == "No"
if scheme_params[:confirmed] == "true" || @scheme.confirmed?
if check_answers && should_direct_via_secondary_client_group_page?(page)
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "check-answers")
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "has-other-client-group")
else
@scheme.locations.update!(confirmed: true)
flash[:notice] = if scheme_previously_confirmed
@ -162,7 +162,7 @@ class SchemesController < ApplicationController
end
elsif check_answers
if should_direct_via_secondary_client_group_page?(page)
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "check-answers")
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "has-other-client-group")
else
redirect_to scheme_check_answers_path(@scheme)
end

18
app/helpers/schemes_helper.rb

@ -131,6 +131,24 @@ module SchemesHelper
messages[attribute[:id]] || "Enter #{text}"
end
def scheme_back_button_path(scheme, current_page)
return scheme_check_answers_path(scheme) if request.params[:referrer] == "check-answers"
return scheme_confirm_secondary_client_group_path(scheme, referrer: "check-answers") if request.params[:referrer] == "has-other-client-group"
case current_page
when "details"
schemes_path
when "primary_client_group"
scheme_details_path(scheme)
when "confirm_secondary_client_group"
scheme_primary_client_group_path(scheme)
when "secondary_client_group"
scheme_confirm_secondary_client_group_path(scheme)
when "support"
scheme.has_other_client_group == "Yes" ? scheme_secondary_client_group_path(scheme) : scheme_confirm_secondary_client_group_path(scheme)
end
end
private
ActivePeriod = Struct.new(:from, :to)

21
app/mailers/bulk_upload_mailer.rb

@ -3,6 +3,7 @@ class BulkUploadMailer < NotifyMailer
COMPLETE_TEMPLATE_ID = "83279578-c890-4168-838b-33c9cf0dc9f0".freeze
FAILED_CSV_ERRORS_TEMPLATE_ID = "e27abcd4-5295-48c2-b127-e9ee4b781b75".freeze
FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID = "931d5bda-a08f-4de6-a455-38a63bff1956".freeze
FAILED_FILE_SETUP_ERROR_TEMPLATE_ID = "24c9f4c7-96ad-470a-ba31-eb51b7cbafd9".freeze
FAILED_SERVICE_ERROR_TEMPLATE_ID = "c3f6288c-7a74-4e77-99ee-6c4a0f6e125a".freeze
HOW_TO_FIX_UPLOAD_TEMPLATE_ID = "21a07b26-f625-4846-9f4d-39e30937aa24".freeze
@ -95,6 +96,26 @@ class BulkUploadMailer < NotifyMailer
)
end
def send_correct_duplicates_and_upload_again_mail(bulk_upload:)
summary_report_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
bulk_upload.sales? ? summary_bulk_upload_sales_result_url(bulk_upload) : summary_bulk_upload_lettings_result_url(bulk_upload)
else
bulk_upload.sales? ? bulk_upload_sales_result_url(bulk_upload) : bulk_upload_lettings_result_url(bulk_upload)
end
send_email(
bulk_upload.user.email,
FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID,
{
filename: bulk_upload.filename,
upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time),
year_combo: bulk_upload.year_combo,
lettings_or_sales: bulk_upload.log_type,
summary_report_link:,
},
)
end
def send_bulk_upload_failed_file_setup_error_mail(bulk_upload:)
bulk_upload_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
bulk_upload.sales? ? summary_bulk_upload_sales_result_url(bulk_upload) : summary_bulk_upload_lettings_result_url(bulk_upload)

9
app/models/derived_variables/lettings_log_variables.rb

@ -84,6 +84,15 @@ module DerivedVariables::LettingsLogVariables
if uprn_known&.zero?
self.uprn = nil
if uprn_known_was == 1
self.address_line1 = nil
self.address_line2 = nil
self.town_or_city = nil
self.county = nil
self.postcode_known = nil
self.postcode_full = nil
self.la = nil
end
end
if uprn_known == 1 && uprn_confirmed&.zero?

9
app/models/derived_variables/sales_log_variables.rb

@ -53,6 +53,15 @@ module DerivedVariables::SalesLogVariables
if uprn_known&.zero?
self.uprn = nil
if uprn_known_was == 1
self.address_line1 = nil
self.address_line2 = nil
self.town_or_city = nil
self.county = nil
self.pcodenk = nil
self.postcode_full = nil
self.la = nil
end
end
if uprn_known == 1 && uprn_confirmed&.zero?

3
app/models/export.rb

@ -1,2 +1,5 @@
class Export < ApplicationRecord
scope :lettings, -> { where(collection: "lettings") }
scope :organisations, -> { where(collection: "organisations") }
scope :users, -> { where(collection: "users") }
end

6
app/models/form/sales/questions/buyer2_working_situation.rb

@ -15,6 +15,10 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
def displayed_answer_options(_log, _user = nil)
answer_options.reject { |key, _| key == "9" }
end
def answer_options
if form.start_year_2025_or_later?
{
@ -26,6 +30,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
"6" => { "value" => "Not seeking work" },
"7" => { "value" => "Full-time student" },
"8" => { "value" => "Unable to work due to long term sick or disability" },
"9" => { "value" => "Child under 16" },
"0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" },
}.freeze
@ -41,6 +46,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
"0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" },
"7" => { "value" => "Full-time student" },
"9" => { "value" => "Child under 16" },
}.freeze
end
end

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

@ -2,7 +2,7 @@ class Form::Sales::Questions::Equity < ::Form::Question
def initialize(id, hsh, page)
super
@id = "equity"
@copy_key = "sales.sale_information.equity.#{page.id}"
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.equity.#{page.id}" : "sales.sale_information.equity"
@type = "numeric"
@min = 0
@max = 100

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

@ -2,7 +2,7 @@ class Form::Sales::Questions::Value < ::Form::Question
def initialize(id, hsh, page)
super
@id = "value"
@copy_key = "sales.sale_information.value.#{page.id}"
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.value.#{page.id}" : "sales.sale_information.value"
@type = "numeric"
@min = 0
@step = 1

39
app/models/forms/bulk_upload_lettings/needstype.rb

@ -1,39 +0,0 @@
module Forms
module BulkUploadLettings
class Needstype
include ActiveModel::Model
include ActiveModel::Attributes
include Rails.application.routes.url_helpers
attribute :needstype, :integer
attribute :year, :integer
attribute :organisation_id, :integer
validates :needstype, presence: true
def view_path
"bulk_upload_lettings_logs/forms/needstype"
end
def options
[OpenStruct.new(id: 1, name: "General needs"), OpenStruct.new(id: 2, name: "Supported housing")]
end
def back_path
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def next_path
bulk_upload_lettings_log_path(id: "upload-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def year_combo
"#{year} to #{year + 1}"
end
def save!
true
end
end
end
end

4
app/models/log.rb

@ -126,9 +126,11 @@ class Log < ApplicationRecord
end
def address_options
return @address_options if @address_options
return @address_options if @address_options && @last_searched_address_string == address_string
if [address_line1_input, postcode_full_input].all?(&:present?)
@last_searched_address_string = address_string
service = AddressClient.new(address_string)
service.call
if service.result.blank? || service.error.present?

1
app/models/organisation.rb

@ -58,6 +58,7 @@ class Organisation < ApplicationRecord
alias_method :la?, :LA?
validates :name, presence: { message: I18n.t("validations.organisation.name_missing") }
validates :name, uniqueness: { case_sensitive: false, message: I18n.t("validations.organisation.name_not_unique") }
validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_missing") }
def self.find_by_id_on_multiple_fields(id)

2
app/models/scheme.rb

@ -329,9 +329,9 @@ class Scheme < ApplicationRecord
def status_at(date)
return :deleted if discarded_at.present?
return :incomplete unless confirmed && locations.confirmed.any?
return :deactivated if owning_organisation.status_at(date) == :deactivated || owning_organisation.status_at(date) == :merged ||
(open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date)
return :incomplete unless confirmed && locations.confirmed.any?
return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date
return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date
return :activating_soon if startdate.present? && date < startdate

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

@ -64,7 +64,7 @@ module Validations::Sales::SaleInformationValidations
if over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: !record.discount.nil?) && record.discounted_ownership_sale?
deposit_and_grant_sentence = record.grant.present? ? ", cash deposit (#{record.field_formatted_as_currency('deposit')}), and grant (#{record.field_formatted_as_currency('grant')})" : " and cash deposit (#{record.field_formatted_as_currency('deposit')})"
discount_sentence = record.discount.present? ? " (#{record.field_formatted_as_currency('value')}) subtracted by the sum of the full purchase price (#{record.field_formatted_as_currency('value')}) multiplied by the percentage discount (#{record.discount}%)" : ""
%i[mortgageused mortgage value deposit ownershipsch discount grant].each do |field|
%i[mortgageused mortgage value deposit discount grant].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.discounted_ownership_value",
mortgage: record.mortgage&.positive? ? " (#{record.field_formatted_as_currency('mortgage')})" : "",
deposit_and_grant_sentence:,
@ -72,6 +72,12 @@ module Validations::Sales::SaleInformationValidations
discount_sentence:,
value_with_discount: record.field_formatted_as_currency("value_with_discount")).html_safe
end
record.errors.add :ownershipsch, :skip_bu_error, message: I18n.t("validations.sales.sale_information.ownershipsch.discounted_ownership_value",
mortgage: record.mortgage&.positive? ? " (#{record.field_formatted_as_currency('mortgage')})" : "",
deposit_and_grant_sentence:,
mortgage_deposit_and_grant_total: record.field_formatted_as_currency("mortgage_deposit_and_grant_total"),
discount_sentence:,
value_with_discount: record.field_formatted_as_currency("value_with_discount")).html_safe
end
end

14
app/services/bulk_upload/lettings/validator.rb

@ -40,29 +40,25 @@ class BulkUpload::Lettings::Validator
end
end
def create_logs?
return false if any_setup_errors?
def block_log_creation_reason
return "setup_errors" if any_setup_errors?
if row_parsers.any?(&:block_log_creation?)
Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.")
return false
return "row_parser_block_log_creation"
end
if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled?
Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.")
return false
return "duplicate_logs"
end
row_parsers.each do |row_parser|
row_parser.log.blank_invalid_non_setup_fields!
end
if any_logs_invalid?
Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.")
return false
"logs_invalid"
end
true
end
def self.question_for_field(field)

20
app/services/bulk_upload/lettings/year2024/row_parser.rb

@ -607,14 +607,22 @@ private
def validate_uprn_exists_if_any_key_address_fields_are_blank
if field_16.blank? && !key_address_fields_provided?
errors.add(:field_16, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "UPRN."))
%i[field_17 field_19 field_21 field_22].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_answered")) if send(field).blank?
end
errors.add(:field_16, I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN."))
end
end
def validate_address_option_found
if log.uprn.nil? && field_16.blank? && key_address_fields_provided?
error_message = if log.address_options_present?
I18n.t("#{ERROR_BASE_KEY}.address.not_determined")
else
I18n.t("#{ERROR_BASE_KEY}.address.not_found")
end
%i[field_17 field_18 field_19 field_20 field_21 field_22].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_found"))
errors.add(field, error_message) if errors[field].blank?
end
end
end
@ -625,19 +633,19 @@ private
def validate_address_fields
if field_16.blank? || log.errors.attribute_names.include?(:uprn)
if field_17.blank?
if field_17.blank? && errors[:field_17].blank?
errors.add(:field_17, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1."))
end
if field_19.blank?
if field_19.blank? && errors[:field_19].blank?
errors.add(:field_19, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city."))
end
if field_21.blank?
if field_21.blank? && errors[:field_21].blank?
errors.add(:field_21, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode."))
end
if field_22.blank?
if field_22.blank? && errors[:field_22].blank?
errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))
end
end

40
app/services/bulk_upload/processor.rb

@ -36,20 +36,28 @@ class BulkUpload::Processor
if validator.any_setup_errors?
send_setup_errors_mail
elsif validator.create_logs?
create_logs
if validator.soft_validation_errors_only?
send_check_soft_validations_mail
elsif created_logs_but_incompleted?
send_how_to_fix_upload_mail
elsif created_logs_and_all_completed?
bulk_upload.unpend
send_success_mail
end
else
send_correct_and_upload_again_mail # summary/full report
block_creation_reason = validator.block_log_creation_reason
if block_creation_reason.present?
case block_creation_reason
when "duplicate_logs"
send_correct_duplicates_and_upload_again_mail
else
send_correct_and_upload_again_mail # summary/full report
end
else
create_logs
if validator.soft_validation_errors_only?
send_check_soft_validations_mail
elsif created_logs_but_incompleted?
send_how_to_fix_upload_mail
elsif created_logs_and_all_completed?
bulk_upload.unpend
send_success_mail
end
end
end
rescue StandardError => e
Sentry.capture_exception(e)
@ -97,6 +105,12 @@ private
.deliver_later
end
def send_correct_duplicates_and_upload_again_mail
BulkUploadMailer
.send_correct_duplicates_and_upload_again_mail(bulk_upload:)
.deliver_later
end
def send_success_mail
BulkUploadMailer
.send_bulk_upload_complete_mail(user:, bulk_upload:)

12
app/services/bulk_upload/sales/validator.rb

@ -39,17 +39,17 @@ class BulkUpload::Sales::Validator
end
end
def create_logs?
return false if any_setup_errors?
def block_log_creation_reason
return "setup_errors" if any_setup_errors?
if row_parsers.any?(&:block_log_creation?)
Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.")
return false
return "row_parser_block_log_creation"
end
if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled?
Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.")
return false
return "duplicate_logs"
end
row_parsers.each do |row_parser|
@ -58,10 +58,8 @@ class BulkUpload::Sales::Validator
if any_logs_invalid?
Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.")
return false
"logs_invalid"
end
true
end
def any_setup_errors?

20
app/services/bulk_upload/sales/year2024/row_parser.rb

@ -605,14 +605,22 @@ private
def validate_uprn_exists_if_any_key_address_fields_are_blank
if field_22.blank? && !key_address_fields_provided?
errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "UPRN."))
%i[field_23 field_25 field_27 field_28].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_answered")) if send(field).blank?
end
errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN."))
end
end
def validate_address_option_found
if log.uprn.nil? && field_22.blank? && key_address_fields_provided?
error_message = if log.address_options_present?
I18n.t("#{ERROR_BASE_KEY}.address.not_determined")
else
I18n.t("#{ERROR_BASE_KEY}.address.not_found")
end
%i[field_23 field_24 field_25 field_26 field_27 field_28].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_found"))
errors.add(field, error_message) if errors[field].blank?
end
end
end
@ -623,19 +631,19 @@ private
def validate_address_fields
if field_22.blank? || log.errors.attribute_names.include?(:uprn)
if field_23.blank?
if field_23.blank? && errors[:field_23].blank?
errors.add(:field_23, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1."))
end
if field_25.blank?
if field_25.blank? && errors[:field_25].blank?
errors.add(:field_25, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city."))
end
if field_27.blank?
if field_27.blank? && errors[:field_27].blank?
errors.add(:field_27, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode."))
end
if field_28.blank?
if field_28.blank? && errors[:field_28].blank?
errors.add(:field_28, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))
end
end

10
app/services/exports/export_service.rb

@ -7,7 +7,7 @@ module Exports
@logger = logger
end
def export_xml(full_update: false, collection: nil)
def export_xml(full_update: false, collection: nil, year: nil)
start_time = Time.zone.now
daily_run_number = get_daily_run_number
lettings_archives_for_manifest = {}
@ -21,12 +21,12 @@ module Exports
when "organisations"
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
else
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, year)
end
else
users_archives_for_manifest = get_user_archives(start_time, full_update)
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, year)
end
write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest).merge(organisations_archives_for_manifest))
@ -70,9 +70,9 @@ module Exports
organisations_export_service.export_xml_organisations(full_update:)
end
def get_lettings_archives(start_time, full_update, collection)
def get_lettings_archives(start_time, full_update, collection_year)
lettings_export_service = Exports::LettingsLogExportService.new(@storage_service, start_time)
lettings_export_service.export_xml_lettings_logs(full_update:, collection_year: collection)
lettings_export_service.export_xml_lettings_logs(full_update:, collection_year:)
end
end
end

22
app/services/exports/lettings_log_export_service.rb

@ -5,11 +5,11 @@ module Exports
def export_xml_lettings_logs(full_update: false, collection_year: nil)
archives_for_manifest = {}
collection_years_to_export(collection_year).each do |collection|
recent_export = Export.where(collection:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives = write_export_archive(export, collection, recent_export, full_update)
collection_years_to_export(collection_year).each do |year|
recent_export = Export.lettings.where(year:).order("started_at").last
base_number = Export.lettings.where(empty_export: false, year:).maximum(:base_number) || 1
export = build_export_run("lettings", base_number, full_update, year)
archives = write_export_archive(export, year, recent_export, full_update)
archives_for_manifest.merge!(archives)
@ -22,21 +22,21 @@ module Exports
private
def get_archive_name(collection, base_number, increment)
return unless collection
def get_archive_name(year, base_number, increment)
return unless year
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"core_#{collection}_#{collection + 1}_apr_mar_#{base_number_str}_#{increment_str}".downcase
"core_#{year}_#{year + 1}_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, collection)
def retrieve_resources(recent_export, full_update, year)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
LettingsLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params).filter_by_year(collection)
LettingsLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params).filter_by_year(year)
else
params = { to: @start_time }
LettingsLog.exportable.where("updated_at <= :to", params).filter_by_year(collection)
LettingsLog.exportable.where("updated_at <= :to", params).filter_by_year(year)
end
end

12
app/services/exports/organisation_export_service.rb

@ -5,9 +5,9 @@ module Exports
def export_xml_organisations(full_update: false)
collection = "organisations"
recent_export = Export.where(collection:).order("started_at").last
recent_export = Export.organisations.order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
base_number = Export.organisations.where(empty_export: false).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
@ -19,15 +19,13 @@ module Exports
private
def get_archive_name(collection, base_number, increment)
return unless collection
def get_archive_name(_year, base_number, increment)
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"#{collection}_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
"organisations_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, _collection)
def retrieve_resources(recent_export, full_update, _year)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
Organisation.where("(updated_at >= :from AND updated_at <= :to)", params)

12
app/services/exports/user_export_service.rb

@ -5,9 +5,9 @@ module Exports
def export_xml_users(full_update: false)
collection = "users"
recent_export = Export.where(collection:).order("started_at").last
recent_export = Export.users.order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
base_number = Export.users.where(empty_export: false).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
@ -19,15 +19,13 @@ module Exports
private
def get_archive_name(collection, base_number, increment)
return unless collection
def get_archive_name(_year, base_number, increment)
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"#{collection}_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
"users_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, _collection)
def retrieve_resources(recent_export, full_update, _year)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
User.where("(updated_at >= :from AND updated_at <= :to)", params)

20
app/services/exports/xml_export_service.rb

@ -11,9 +11,9 @@ module Exports
private
def build_export_run(collection, base_number, full_update)
@logger.info("Building export run for #{collection}")
previous_exports_with_data = Export.where(collection:, empty_export: false)
def build_export_run(collection, base_number, full_update, year = nil)
@logger.info("Building export run for #{[collection, year].join(' ')}")
previous_exports_with_data = Export.where(collection:, year:, empty_export: false)
increment_number = previous_exports_with_data.where(base_number:).maximum(:increment_number) || 1
@ -25,16 +25,16 @@ module Exports
end
if previous_exports_with_data.empty?
return Export.new(collection:, base_number:, started_at: @start_time)
return Export.new(collection:, year:, base_number:, started_at: @start_time)
end
Export.new(collection:, started_at: @start_time, base_number:, increment_number:)
Export.new(collection:, year:, started_at: @start_time, base_number:, increment_number:)
end
def write_export_archive(export, collection, recent_export, full_update)
archive = get_archive_name(collection, export.base_number, export.increment_number) # archive name would be the same for all logs because they're already filtered by year (?)
def write_export_archive(export, year, recent_export, full_update)
archive = get_archive_name(year, export.base_number, export.increment_number)
initial_count = retrieve_resources(recent_export, full_update, collection).count
initial_count = retrieve_resources(recent_export, full_update, year).count
@logger.info("Creating #{archive} - #{initial_count} resources")
return {} if initial_count.zero?
@ -46,12 +46,12 @@ module Exports
loop do
slice = if last_processed_marker.present?
retrieve_resources(recent_export, full_update, collection)
retrieve_resources(recent_export, full_update, year)
.where("created_at > ?", last_processed_marker)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
else
retrieve_resources(recent_export, full_update, collection)
retrieve_resources(recent_export, full_update, year)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
end

23
app/views/bulk_upload_lettings_logs/forms/needstype.erb

@ -1,23 +0,0 @@
<% content_for :before_content do %>
<%= govuk_back_link href: @form.back_path %>
<% end %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "needstype"), method: :patch do |f| %>
<%= f.govuk_error_summary %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %>
<%= f.govuk_collection_radio_buttons :needstype,
@form.options,
:id,
:name,
hint: { text: I18n.t("hints.bulk_upload.needstype") },
legend: { text: "What is the needs type?", size: "l" },
caption: { text: "Upload lettings logs in bulk (#{@form.year_combo})", size: "l" } %>
<%= f.govuk_submit %>
<% end %>
</div>
</div>

2
app/views/bulk_upload_shared/uploads.html.erb

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "uploads") %>
<% item_label = format_label(@pagy.count, "upload") %>
<% title = format_title(@searched, bulk_upload_title(controller.controller_name), current_user, item_label, @pagy.count, nil) %>
<% content_for :title, title %>

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

@ -32,7 +32,7 @@
count: @pagy.count,
item_label:,
total_count: @total_count,
item: "locations",
item: "location",
filters_count: applied_filters_count(@filter_type),
)) %>
<% end %>

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

@ -1,7 +1,7 @@
<h2 class="govuk-body">
<div class="govuk-grid-row app-search__caption">
<div class="govuk-grid-column-three-quarters">
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "logs", filters_count: applied_filters_count(@filter_type))) %>
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "log", filters_count: applied_filters_count(@filter_type))) %>
<% if logs&.any? %>
<%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %>
<% if @current_user.support? %>

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

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "logs") %>
<% item_label = format_label(@pagy.count, "log") %>
<% title = format_title(@searched, "#{log_type_for_controller(controller).capitalize} logs", current_user, item_label, @pagy.count, nil) %>
<% content_for :title, title %>

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

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "logs") %>
<% item_label = format_label(@pagy.count, "log") %>
<% title = "Update logs" %>
<% content_for :title, title %>

4
app/views/organisation_relationships/managing_agents.html.erb

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "managing agents") %>
<% item_label = format_label(@pagy.count, "managing agent") %>
<% if current_user.support? %>
<%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %>
@ -44,7 +44,7 @@
pagy: @pagy,
searched: @searched,
item_label:,
search_item: "managing agents",
search_item: "managing agent",
total_count: @total_count,
remove_path: ->(org_id) { managing_agents_remove_organisation_path(target_organisation_id: org_id) },
} %>

4
app/views/organisation_relationships/stock_owners.html.erb

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "stock owners") %>
<% item_label = format_label(@pagy.count, "stock owner") %>
<% if current_user.support? %>
<%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %>
<%= render SubNavigationComponent.new(items: secondary_items(request.path, @organisation.id)) %>
@ -41,7 +41,7 @@
pagy: @pagy,
searched: @searched,
item_label:,
search_item: "stock owners",
search_item: "stock owner",
total_count: @total_count,
remove_path: ->(org_id) { stock_owners_remove_organisation_path(target_organisation_id: org_id) },
} %>

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

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

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

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "organisations") %>
<% item_label = format_label(@pagy.count, "organisation") %>
<% title = format_title(@searched, "Organisations", current_user, item_label, @pagy.count, nil) %>
<% content_for :title, title %>

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

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "logs") %>
<% item_label = format_label(@pagy.count, "log") %>
<% title = format_title(@searched, action_name.humanize, current_user, item_label, @pagy.count, @organisation.name) %>
<% content_for :title, title %>

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

@ -2,7 +2,7 @@
<%= govuk_table do |table| %>
<%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<span class="app-search__caption">
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "schemes", filters_count: applied_filters_count(@filter_type))) %>
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "scheme", filters_count: applied_filters_count(@filter_type))) %>
<% if @schemes&.any? %>
<%= govuk_link_to "Download schemes (CSV)", schemes_csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %>
<%= govuk_link_to "Download locations (CSV)", locations_csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %>

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

@ -1,7 +1,7 @@
<% content_for :title, "Does this scheme provide for another client group?" %>
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<%= govuk_back_link(href: scheme_back_button_path(@scheme, "confirm_secondary_client_group")) %>
<% end %>
<%= render partial: "organisations/headings", locals: { main: "Does this scheme provide for another client group?", sub: @scheme.service_name } %>

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

@ -1,7 +1,7 @@
<% content_for :title, "Create a new supported housing scheme" %>
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<%= govuk_back_link(href: scheme_back_button_path(@scheme, "details")) %>
<% end %>
<%= form_for(@scheme, method: :patch) do |f| %>

8
app/views/schemes/primary_client_group.html.erb

@ -1,13 +1,7 @@
<% content_for :title, "What client group is this scheme intended for?" %>
<% if request.referer&.include?("new") %>
<% back_button_path = scheme_details_path(@scheme) %>
<% else %>
<% back_button_path = :back %>
<% end %>
<% content_for :before_content do %>
<%= govuk_back_link(href: back_button_path) %>
<%= govuk_back_link(href: scheme_back_button_path(@scheme, "primary_client_group")) %>
<% end %>
<%= form_for(@scheme, method: :patch) do |f| %>

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

@ -1,7 +1,7 @@
<% content_for :title, "What is the other client group?" %>
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<%= govuk_back_link(href: scheme_back_button_path(@scheme, "secondary_client_group")) %>
<% end %>
<%= form_for(@scheme, method: :patch) do |f| %>

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

@ -1,7 +1,7 @@
<% content_for :title, "What support does this scheme provide?" %>
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<%= govuk_back_link(href: scheme_back_button_path(@scheme, "support")) %>
<% end %>
<%= form_for(@scheme, method: :patch) do |f| %>

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

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

1
config/locales/en.yml

@ -235,6 +235,7 @@ en:
organisation:
data_sharing_agreement_not_signed: "Your organisation must accept the Data Sharing Agreement before you can create any logs."
name_missing: "Enter the name of the organisation."
name_not_unique: "An organisation with this name already exists. Use the organisation that already exists or add a location or other identifier to the name. For example: Organisation name (City)."
provider_type_missing: "Select the organisation type."
stock_owner:
blank: "You must choose a stock owner."

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

@ -22,7 +22,7 @@ en:
age1_known:
check_answer_label: ""
check_answer_prompt: "Answer if you know the lead tenant's age"
hint_text: "The lead’ or ’main’ tenant is the person in the household who does the most paid work. If several people do the same paid work, the lead tenant is whoever is the oldest."
hint_text: "The lead tenant is the person in the household who does the most paid work. If several people do the same paid work, the lead tenant is whoever is the oldest."
question_text: "Do you know the lead tenant’s age?"
age1:
check_answer_label: "Lead tenant’s age"

40
config/locales/forms/2023/sales/household_characteristics.en.yml

@ -120,10 +120,10 @@ en:
question_text: "What is buyer 2's relationship to buyer 1?"
person:
page_header: ""
check_answer_label: "Person 2’s relationship to Buyer 1"
check_answer_label: "Person 2’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 2’s relationship to Buyer 1?"
question_text: "What is person 2’s relationship to buyer 1?"
age2:
buyer:
@ -163,7 +163,7 @@ en:
check_answer_label: "Person 2’s gender identity"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 2’s gender identity?"
question_text: "Which of these best describes person 2’s gender identity?"
ethnic_group2:
page_header: ""
@ -223,7 +223,7 @@ en:
check_answer_label: "Person 2’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 2’s working situation?"
question_text: "Which of these best describes person 2’s working situation?"
buy2livein:
page_header: ""
@ -262,10 +262,10 @@ en:
relat3:
page_header: ""
check_answer_label: "Person 3’s relationship to Buyer 1"
check_answer_label: "Person 3’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 3’s relationship to Buyer 1?"
question_text: "What is person 3’s relationship to buyer 1?"
age3:
page_header: ""
@ -285,14 +285,14 @@ en:
check_answer_label: "Person 3’s gender identity"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 3’s gender identity?"
question_text: "Which of these best describes person 3’s gender identity?"
ecstat3:
page_header: ""
check_answer_label: "Person 3’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 3’s working situation?"
question_text: "Which of these best describes person 3’s working situation?"
details_known_4:
page_header: ""
@ -303,10 +303,10 @@ en:
relat4:
page_header: ""
check_answer_label: "Person 4’s relationship to Buyer 1"
check_answer_label: "Person 4’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 4’s relationship to Buyer 1?"
question_text: "What is person 4’s relationship to buyer 1?"
age4:
page_header: ""
@ -326,14 +326,14 @@ en:
check_answer_label: "Person 4’s gender identity"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 4’s gender identity?"
question_text: "Which of these best describes person 4’s gender identity?"
ecstat4:
page_header: ""
check_answer_label: "Person 4’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 4’s working situation?"
question_text: "Which of these best describes person 4’s working situation?"
details_known_5:
page_header: ""
@ -344,10 +344,10 @@ en:
relat5:
page_header: ""
check_answer_label: "Person 5’s relationship to Buyer 1"
check_answer_label: "Person 5’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 5’s relationship to Buyer 1?"
question_text: "What is person 5’s relationship to buyer 1?"
age5:
page_header: ""
@ -367,14 +367,14 @@ en:
check_answer_label: "Person 5’s gender identity"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 5’s gender identity?"
question_text: "Which of these best describes person 5’s gender identity?"
ecstat5:
page_header: ""
check_answer_label: "Person 5’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 5’s working situation?"
question_text: "Which of these best describes person 5’s working situation?"
details_known_6:
page_header: ""
@ -385,10 +385,10 @@ en:
relat6:
page_header: ""
check_answer_label: "Person 6’s relationship to Buyer 1"
check_answer_label: "Person 6’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 6’s relationship to Buyer 1?"
question_text: "What is person 6’s relationship to buyer 1?"
age6:
page_header: ""
@ -408,11 +408,11 @@ en:
check_answer_label: "Person 6’s gender identity"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 6’s gender identity?"
question_text: "Which of these best describes person 6’s gender identity?"
ecstat6:
page_header: ""
check_answer_label: "Person 6’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 6’s working situation?"
question_text: "Which of these best describes person 6’s working situation?"

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

@ -47,13 +47,11 @@ en:
joint_purchase:
page_header: ""
check_answer_label: "Housing-related benefits buyers received before buying this property"
check_answer_prompt: ""
hint_text: ""
question_text: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?"
not_joint_purchase:
page_header: ""
check_answer_label: "Housing-related benefits buyer received before buying this property"
check_answer_prompt: ""
hint_text: ""
question_text: "Was the buyer receiving any of these housing-related benefits immediately before buying this property?"
@ -61,10 +59,10 @@ en:
joint_purchase:
page_header: ""
savingsnk:
check_answer_label: "Buyers’ total savings known?"
check_answer_label: "Buyers’ total savings known"
check_answer_prompt: ""
hint_text: ""
question_text: "Do you know how much the 'buyers' had in savings before they paid any deposit for the property?"
question_text: "Do you know how much the buyers had in savings before they paid any deposit for the property?"
savings:
check_answer_label: "Buyers’ total savings before any deposit paid"
check_answer_prompt: ""
@ -73,7 +71,7 @@ en:
not_joint_purchase:
page_header: ""
savingsnk:
check_answer_label: "Buyer’s total savings known?"
check_answer_label: "Buyer’s total savings known"
check_answer_prompt: ""
hint_text: ""
question_text: "Do you know how much the buyer had in savings before they paid any deposit for the property?"

11
config/locales/forms/2023/sales/sale_information.en.yml

@ -33,7 +33,7 @@ en:
page_header: ""
check_answer_label: "Staircasing transaction"
check_answer_prompt: ""
hint_text: "A staircasing transaction is when the household purchases more shares in their property, increasing the proportion they own and decreasing the proportion the housing association owns. Once the household purchases 100% of the shares, they own the property"
hint_text: "A staircasing transaction is when the household purchases more shares in their property, increasing the proportion they own and decreasing the proportion the housing association owns. Once the household purchases 100% of the shares, they own the property."
question_text: "Is this a staircasing transaction?"
about_staircasing:
page_header: "About the staircasing transaction"
@ -86,7 +86,6 @@ en:
joint_purchase:
page_header: ""
check_answer_label: "Any buyers were registered providers, housing association or local authority tenants immediately before this sale?"
check_answer_prompt: ""
hint_text: ""
question_text: "Were any of the buyers private registered providers, housing association or local authority tenants immediately before this sale?"
not_joint_purchase:
@ -97,10 +96,10 @@ en:
question_text: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
frombeds:
page_header: "About the buyers’ previous property"
page_header: ""
check_answer_label: "Number of bedrooms in previous property"
check_answer_prompt: ""
hint_text: "For bedsits enter 1"
hint_text: "A bedsit has 1 bedroom."
question_text: "How many bedrooms did the property have?"
fromprop:
@ -132,14 +131,14 @@ en:
question_text: "What was the initial percentage equity stake purchased?"
mortgageused:
page_header: "Mortgage Amount"
page_header: ""
check_answer_label: "Mortgage used"
check_answer_prompt: ""
hint_text: ""
question_text: "Was a mortgage used for the purchase of this property?"
mortgage:
page_header: "Mortgage Amount"
page_header: ""
check_answer_label: "Mortgage amount"
check_answer_prompt: ""
hint_text: "Enter the amount of mortgage agreed with the mortgage lender. Exclude any deposits or cash payments. Numeric in pounds. Rounded to the nearest pound."

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

@ -15,7 +15,7 @@ en:
age1_known:
check_answer_label: ""
check_answer_prompt: "Answer if you know the lead tenant's age"
hint_text: "The lead’ or ’main’ tenant is the person in the household who does the most paid work. If several people do the same amount of paid work, the lead tenant is whoever is the oldest."
hint_text: "The lead tenant is the person in the household who does the most paid work. If several people do the same paid work, the lead tenant is whoever is the oldest."
question_text: "Do you know the lead tenant’s age?"
age1:
check_answer_label: "Lead tenant’s age"

40
config/locales/forms/2024/sales/household_characteristics.en.yml

@ -99,10 +99,10 @@ en:
question_text: "What is buyer 2's relationship to buyer 1?"
person:
page_header: ""
check_answer_label: "Person 2’s relationship to Buyer 1"
check_answer_label: "Person 2’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 2’s relationship to Buyer 1?"
question_text: "What is person 2’s relationship to buyer 1?"
age2:
buyer:
@ -142,7 +142,7 @@ en:
check_answer_label: "Person 2’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 2’s gender identity?"
question_text: "Which of these best describes person 2’s gender identity?"
ethnic_group2:
page_header: ""
@ -209,7 +209,7 @@ en:
check_answer_label: "Person 2’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 2’s working situation?"
question_text: "Which of these best describes person 2’s working situation?"
buy2livein:
page_header: ""
@ -248,10 +248,10 @@ en:
relat3:
page_header: ""
check_answer_label: "Person 3’s relationship to Buyer 1"
check_answer_label: "Person 3’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 3’s relationship to Buyer 1?"
question_text: "What is person 3’s relationship to buyer 1?"
age3:
page_header: ""
@ -271,14 +271,14 @@ en:
check_answer_label: "Person 3’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 3’s gender identity?"
question_text: "Which of these best describes person 3’s gender identity?"
ecstat3:
page_header: ""
check_answer_label: "Person 3’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 3’s working situation?"
question_text: "Which of these best describes person 3’s working situation?"
details_known_4:
page_header: ""
@ -289,10 +289,10 @@ en:
relat4:
page_header: ""
check_answer_label: "Person 4’s relationship to Buyer 1"
check_answer_label: "Person 4’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 4’s relationship to Buyer 1?"
question_text: "What is person 4’s relationship to buyer 1?"
age4:
page_header: ""
@ -312,14 +312,14 @@ en:
check_answer_label: "Person 4’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 4’s gender identity?"
question_text: "Which of these best describes person 4’s gender identity?"
ecstat4:
page_header: ""
check_answer_label: "Person 4’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 4’s working situation?"
question_text: "Which of these best describes person 4’s working situation?"
details_known_5:
page_header: ""
@ -330,10 +330,10 @@ en:
relat5:
page_header: ""
check_answer_label: "Person 5’s relationship to Buyer 1"
check_answer_label: "Person 5’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 5’s relationship to Buyer 1?"
question_text: "What is person 5’s relationship to buyer 1?"
age5:
page_header: ""
@ -353,14 +353,14 @@ en:
check_answer_label: "Person 5’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 5’s gender identity?"
question_text: "Which of these best describes person 5’s gender identity?"
ecstat5:
page_header: ""
check_answer_label: "Person 5’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 5’s working situation?"
question_text: "Which of these best describes person 5’s working situation?"
details_known_6:
page_header: ""
@ -371,10 +371,10 @@ en:
relat6:
page_header: ""
check_answer_label: "Person 6’s relationship to Buyer 1"
check_answer_label: "Person 6’s relationship to buyer 1"
check_answer_prompt: ""
hint_text: ""
question_text: "What is Person 6’s relationship to Buyer 1?"
question_text: "What is person 6’s relationship to buyer 1?"
age6:
page_header: ""
@ -394,11 +394,11 @@ en:
check_answer_label: "Person 6’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 6’s gender identity?"
question_text: "Which of these best describes person 6’s gender identity?"
ecstat6:
page_header: ""
check_answer_label: "Person 6’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 6’s working situation?"
question_text: "Which of these best describes person 6’s working situation?"

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

@ -64,7 +64,7 @@ en:
check_answer_label: "Buyers’ total savings known?"
check_answer_prompt: ""
hint_text: ""
question_text: "Do you know how much the 'buyers' had in savings before they paid any deposit for the property?"
question_text: "Do you know how much the buyers had in savings before they paid any deposit for the property?"
savings:
check_answer_label: "Buyers’ total savings before any deposit paid"
check_answer_prompt: ""

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

@ -33,7 +33,7 @@ en:
page_header: ""
check_answer_label: "Staircasing transaction"
check_answer_prompt: ""
hint_text: "A staircasing transaction is when the household purchases more shares in their property, increasing the proportion they own and decreasing the proportion the housing association owns. Once the household purchases 100% of the shares, they own the property"
hint_text: "A staircasing transaction is when the household purchases more shares in their property, increasing the proportion they own and decreasing the proportion the housing association owns. Once the household purchases 100% of the shares, they own the property."
question_text: "Is this a staircasing transaction?"
about_staircasing:
page_header: "About the staircasing transaction"
@ -102,10 +102,10 @@ en:
question_text: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
frombeds:
page_header: "About the buyers’ previous property"
page_header: ""
check_answer_label: "Number of bedrooms in previous property"
check_answer_prompt: ""
hint_text: "For bedsits enter 1"
hint_text: "A bedsit has 1 bedroom."
question_text: "How many bedrooms did the property have?"
fromprop:
@ -137,14 +137,14 @@ en:
question_text: "What was the initial percentage equity stake purchased?"
mortgageused:
page_header: "Mortgage Amount"
page_header: ""
check_answer_label: "Mortgage used"
check_answer_prompt: ""
hint_text: ""
question_text: "Was a mortgage used for the purchase of this property?"
mortgage:
page_header: "Mortgage Amount"
page_header: ""
check_answer_label: "Mortgage amount"
check_answer_prompt: ""
hint_text: "Enter the amount of mortgage agreed with the mortgage lender. Exclude any deposits or cash payments. Numeric in pounds. Rounded to the nearest pound."

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

@ -15,7 +15,7 @@ en:
age1_known:
check_answer_label: ""
check_answer_prompt: "Answer if you know the lead tenant's age"
hint_text: "The lead’ or ’main’ tenant is the person in the household who does the most paid work. If several people do the same amount of paid work, the lead tenant is whoever is the oldest."
hint_text: "The lead tenant is the person in the household who does the most paid work. If several people do the same paid work, the lead tenant is whoever is the oldest."
question_text: "Do you know the lead tenant’s age?"
age1:
check_answer_label: "Lead tenant’s age"

20
config/locales/forms/2025/sales/household_characteristics.en.yml

@ -142,7 +142,7 @@ en:
check_answer_label: "Person 2’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 2’s gender identity?"
question_text: "Which of these best describes person 2’s gender identity?"
ethnic_group2:
page_header: ""
@ -209,7 +209,7 @@ en:
check_answer_label: "Person 2’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 2’s working situation?"
question_text: "Which of these best describes person 2’s working situation?"
buy2livein:
page_header: ""
@ -271,14 +271,14 @@ en:
check_answer_label: "Person 3’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 3’s gender identity?"
question_text: "Which of these best describes person 3’s gender identity?"
ecstat3:
page_header: ""
check_answer_label: "Person 3’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 3’s working situation?"
question_text: "Which of these best describes person 3’s working situation?"
details_known_4:
page_header: ""
@ -312,14 +312,14 @@ en:
check_answer_label: "Person 4’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 4’s gender identity?"
question_text: "Which of these best describes person 4’s gender identity?"
ecstat4:
page_header: ""
check_answer_label: "Person 4’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 4’s working situation?"
question_text: "Which of these best describes person 4’s working situation?"
details_known_5:
page_header: ""
@ -353,14 +353,14 @@ en:
check_answer_label: "Person 5’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 5’s gender identity?"
question_text: "Which of these best describes person 5’s gender identity?"
ecstat5:
page_header: ""
check_answer_label: "Person 5’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 5’s working situation?"
question_text: "Which of these best describes person 5’s working situation?"
details_known_6:
page_header: ""
@ -394,11 +394,11 @@ en:
check_answer_label: "Person 6’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes Person 6’s gender identity?"
question_text: "Which of these best describes person 6’s gender identity?"
ecstat6:
page_header: ""
check_answer_label: "Person 6’s working situation"
check_answer_prompt: ""
hint_text: ""
question_text: "Which of these best describes Person 6’s working situation?"
question_text: "Which of these best describes person 6’s working situation?"

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

@ -64,7 +64,7 @@ en:
check_answer_label: "Buyers’ total savings known?"
check_answer_prompt: ""
hint_text: ""
question_text: "Do you know how much the 'buyers' had in savings before they paid any deposit for the property?"
question_text: "Do you know how much the buyers had in savings before they paid any deposit for the property?"
savings:
check_answer_label: "Buyers’ total savings before any deposit paid"
check_answer_prompt: ""

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

@ -123,10 +123,10 @@ en:
question_text: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
frombeds:
page_header: "About the buyers’ previous property"
page_header: ""
check_answer_label: "Number of bedrooms in previous property"
check_answer_prompt: ""
hint_text: "For bedsits enter 1"
hint_text: "A bedsit has 1 bedroom."
question_text: "How many bedrooms did the property have?"
fromprop:
@ -170,14 +170,14 @@ en:
question_text: "What was the percentage shared purchased in the initial transaction?"
mortgageused:
page_header: "Mortgage Amount"
page_header: ""
check_answer_label: "Mortgage used"
check_answer_prompt: ""
check_answer_prompt: "Answer if a mortgage was used"
hint_text: ""
question_text: "Was a mortgage used for the purchase of this property?"
mortgage:
page_header: "Mortgage Amount"
page_header: ""
check_answer_label: "Mortgage amount"
check_answer_prompt: ""
hint_text: "Enter the amount of mortgage agreed with the mortgage lender. Exclude any deposits or cash payments. Numeric in pounds. Rounded to the nearest pound."

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

@ -49,7 +49,7 @@ en:
page_header: ""
check_answer_label: "Staircasing transaction"
check_answer_prompt: ""
hint_text: "A staircasing transaction is when the household purchases more shares in their property, increasing the proportion they own and decreasing the proportion the housing association owns. Once the household purchases 100% of the shares, they own the property"
hint_text: "A staircasing transaction is when the household purchases more shares in their property, increasing the proportion they own and decreasing the proportion the housing association owns. Once the household purchases 100% of the shares, they own the property."
question_text: "Is this a staircasing transaction?"
type:

2
config/locales/validations/lettings/2024/bulk_upload.en.yml

@ -50,6 +50,8 @@ en:
invalid: "Age of person %{person_num} must be a number or the letter R"
address:
not_found: "We could not find this address. Check the address data in your CSV file is correct and complete, or select the correct address using the CORE site."
not_determined: "There are multiple matches for this address. Either select the correct address manually or correct the UPRN in the CSV file."
not_answered: "Enter either the UPRN or the full address."
nationality:
invalid: "Select a valid nationality."
charges:

2
config/locales/validations/sales/2024/bulk_upload.en.yml

@ -40,5 +40,7 @@ en:
buyer_cannot_be_over_16_and_child: "Buyer 2's age cannot be 16 or over if their working situation is child under 16."
address:
not_found: "We could not find this address. Check the address data in your CSV file is correct and complete, or select the correct address using the CORE site."
not_determined: "There are multiple matches for this address. Either select the correct address manually or correct the UPRN in the CSV file."
not_answered: "Enter either the UPRN or the full address."
nationality:
invalid: "Select a valid nationality."

5
db/migrate/20241125153349_add_unique_index_to_org_name.rb

@ -0,0 +1,5 @@
class AddUniqueIndexToOrgName < ActiveRecord::Migration[7.0]
def change
add_index :organisations, :name, unique: true
end
end

5
db/migrate/20241204100518_add_year_to_export.rb

@ -0,0 +1,5 @@
class AddYearToExport < ActiveRecord::Migration[7.0]
def change
add_column :exports, :year, :integer
end
end

4
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_11_22_154743) do
ActiveRecord::Schema[7.0].define(version: 2024_12_04_100518) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -113,6 +113,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_22_154743) do
t.integer "increment_number", default: 1, null: false
t.boolean "empty_export", default: false, null: false
t.string "collection"
t.integer "year"
end
create_table "la_rent_ranges", force: :cascade do |t|
@ -545,6 +546,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_22_154743) do
t.datetime "discarded_at"
t.datetime "schemes_deduplicated_at"
t.index ["absorbing_organisation_id"], name: "index_organisations_on_absorbing_organisation_id"
t.index ["name"], name: "index_organisations_on_name", unique: true
t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true
end

6
lib/tasks/data_export.rake

@ -7,13 +7,13 @@ namespace :core do
end
desc "Export all data XMLs for import into Central Data System (CDS)"
task :full_data_export_xml, %i[collection] => :environment do |_task, args|
task :full_data_export_xml, %i[collection year] => :environment do |_task, args|
collection = args[:collection].presence
collection = collection.to_i if collection.present? && collection.scan(/\D/).empty?
year = args[:year]&.to_i
storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["EXPORT_BUCKET"])
export_service = Exports::ExportService.new(storage_service)
export_service.export_xml(full_update: true, collection:)
export_service.export_xml(full_update: true, collection:, year:)
end
end

8
lib/tasks/set_export_collection_years.rake

@ -0,0 +1,8 @@
desc "Set export collection years for lettings exports"
task set_export_collection_years: :environment do
Export.where(collection: %w[2022 2023 2024 2025]).find_each do |export|
export.year = export.collection.to_i
export.collection = "lettings"
export.save!
end
end

26
spec/components/search_result_caption_component_spec.rb

@ -5,7 +5,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do
let(:count) { 2 }
let(:item_label) { "user" }
let(:total_count) { 3 }
let(:item) { "schemes" }
let(:item) { "scheme" }
let(:filters_count) { 1 }
let(:result) { render_inline(described_class.new(searched:, count:, item_label:, total_count:, item:, filters_count:)) }
@ -21,6 +21,14 @@ RSpec.describe SearchResultCaptionComponent, type: :component do
it "renders table caption including the search results and total" do
expect(result.to_html).to eq("<span>\n <strong>2</strong> users matching search<br>\n</span>\n")
end
context "with 1 result" do
let(:count) { 1 }
it "renders table caption including the search results and total" do
expect(result.to_html).to eq("<span>\n <strong>1</strong> user matching search<br>\n</span>\n")
end
end
end
context "when filter results are found" do
@ -29,6 +37,14 @@ RSpec.describe SearchResultCaptionComponent, type: :component do
it "renders table caption including the search results and total" do
expect(result.to_html).to eq("<span>\n <strong>2</strong> users matching filters<br>\n</span>\n")
end
context "with 1 result" do
let(:count) { 1 }
it "renders table caption including the search results and total" do
expect(result.to_html).to eq("<span>\n <strong>1</strong> user matching filters<br>\n</span>\n")
end
end
end
context "when no search/filter is applied" do
@ -38,6 +54,14 @@ RSpec.describe SearchResultCaptionComponent, type: :component do
it "renders table caption with total count only" do
expect(result.to_html).to eq("<span>\n <span class=\"govuk-!-margin-right-4\">\n <strong>2</strong> total schemes\n </span>\n</span>\n")
end
context "with 1 result" do
let(:count) { 1 }
it "renders table caption with total count only" do
expect(result.to_html).to eq("<span>\n <span class=\"govuk-!-margin-right-4\">\n <strong>1</strong> total scheme\n </span>\n</span>\n")
end
end
end
context "when nothing is found" do

7
spec/db/seeds_spec.rb

@ -21,7 +21,8 @@ RSpec.describe "seeding process", type: task do
allow(Rails.env).to receive(:review?).and_return(true)
end
it "sets up correct data" do
# Doing this in one test should save ~2 minutes
it "sets up correct data idempotently" do
expect {
Rails.application.load_seed
}.to change(User, :count)
@ -30,10 +31,6 @@ RSpec.describe "seeding process", type: task do
.and change(Scheme, :count)
.and change(Location, :count)
.and change(LaRentRange, :count)
end
it "is idempotent" do
Rails.application.load_seed
expect {
Rails.application.load_seed

348
spec/features/lettings_log_spec.rb

@ -729,5 +729,353 @@ RSpec.describe "Lettings Log Features" do
expect(duplicate_log.duplicate_set_id).to be_nil
end
end
context "when filling out address fields" do
let(:lettings_log) { create(:lettings_log, :setup_completed, assigned_to: user) }
before do
body = {
results: [
{
DPA: {
"POSTCODE": "AA1 1AA",
"POST_TOWN": "Bristol",
"ORGANISATION_NAME": "Some place",
},
},
],
}.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=111")
.to_return(status: 200, body:, headers: {})
body = { results: [{ DPA: { UPRN: "111" } }] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AA&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA11AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 1AA\",\"admin_district\":\"Westminster\",\"codes\":{\"admin_district\":\"E09000033\"}}}", headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA12AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 2AA\",\"admin_district\":\"Wigan\",\"codes\":{\"admin_district\":\"E08000010\"}}}", headers: {})
body = { results: [] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AB&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
visit("/lettings-logs/#{lettings_log.id}/uprn")
end
context "and uprn is known and answered" do
before do
choose "Yes"
fill_in("lettings_log[uprn]", with: "111")
click_button("Save and continue")
end
context "and uprn is confirmed" do
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1) # yes
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
choose "Yes"
click_button("Save and continue")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1) # yes
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(1) # yes
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
context "and changes to uprn not known" do
it "sets correct address fields" do
visit("/lettings-logs/#{lettings_log.id}/uprn")
choose "No"
click_button("Save and continue")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
end
end
context "and uprn is not confirmed" do
before do
choose "No, I want to search for the address instead"
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
end
end
context "and uprn is not known" do
before do
choose "No"
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
context "and the address is not found" do
it "sets correct address fields" do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AB")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
click_button("Confirm and continue")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AB")
expect(lettings_log.address_search_value_check).to eq(0)
expect(lettings_log.la).to eq(nil)
end
end
context "and address is found, re-searched and not found" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
visit("/lettings-logs/#{lettings_log.id}/address-matcher")
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
end
it "routes to the correct page" do
expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/no-address-found")
end
end
context "and the user selects 'address_not_listed'" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq("uprn_not_listed")
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Address line 1")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
context "and the user enters a new address manually" do
context "without changing a valid postcode" do
before do
fill_in("lettings_log[town_or_city]", with: "Town")
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq("uprn_not_listed")
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Address line 1")
expect(lettings_log.address_line2).to eq("")
expect(lettings_log.town_or_city).to eq("Town")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
end
context "with changing the postcode" do
before do
fill_in("lettings_log[town_or_city]", with: "Town")
fill_in("lettings_log[postcode_full]", with: "AA12AA")
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq("uprn_not_listed")
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 2AA")
expect(lettings_log.address_line1).to eq("Address line 1")
expect(lettings_log.address_line2).to eq("")
expect(lettings_log.town_or_city).to eq("Town")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E08000010")
end
end
end
end
context "and the user selects 'address_not_listed' and then changes their mind and selects an address" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
visit("/lettings-logs/#{lettings_log.id}/uprn-selection")
choose("lettings-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1)
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(1)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
end
context "and possible addresses found and selected" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose("lettings-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1)
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(1)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
end
end
end
end
end

348
spec/features/sales_log_spec.rb

@ -310,6 +310,354 @@ RSpec.describe "Sales Log Features" do
expect(page).to have_current_path("/sales-logs/bulk-uploads")
end
end
context "when filling out address fields" do
let(:sales_log) { create(:sales_log, :shared_ownership_setup_complete, assigned_to: user) }
before do
body = {
results: [
{
DPA: {
"POSTCODE": "AA1 1AA",
"POST_TOWN": "Bristol",
"ORGANISATION_NAME": "Some place",
},
},
],
}.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=111")
.to_return(status: 200, body:, headers: {})
body = { results: [{ DPA: { UPRN: "111" } }] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AA&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA11AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 1AA\",\"admin_district\":\"Westminster\",\"codes\":{\"admin_district\":\"E09000033\"}}}", headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA12AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 2AA\",\"admin_district\":\"Wigan\",\"codes\":{\"admin_district\":\"E08000010\"}}}", headers: {})
body = { results: [] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AB&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
visit("/sales-logs/#{sales_log.id}/uprn")
end
context "and uprn is known and answered" do
before do
choose "Yes"
fill_in("sales_log[uprn]", with: "111")
click_button("Save and continue")
end
context "and uprn is confirmed" do
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(1) # yes
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
choose "Yes"
click_button("Save and continue")
sales_log.reload
expect(sales_log.uprn_known).to eq(1) # yes
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(1) # yes
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
context "and changes to uprn not known" do
it "sets correct address fields" do
visit("/sales-logs/#{sales_log.id}/uprn")
choose "No"
click_button("Save and continue")
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
end
end
end
context "and uprn is not confirmed" do
before do
choose "No, I want to search for the address instead"
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
end
end
end
context "and uprn is not known" do
before do
choose "No"
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
end
context "and the address is not found" do
it "sets correct address fields" do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AB")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
click_button("Confirm and continue")
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AB")
expect(sales_log.address_search_value_check).to eq(0)
expect(sales_log.la).to eq(nil)
end
end
context "and address is found, re-searched and not found" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
visit("/sales-logs/#{sales_log.id}/address-matcher")
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
end
it "routes to the correct page" do
expect(page).to have_current_path("/sales-logs/#{sales_log.id}/no-address-found")
end
end
context "and the user selects 'address_not_listed'" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq("uprn_not_listed")
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
context "and the user enters a new address manually" do
context "without changing a valid postcode" do
before do
fill_in("sales_log[town_or_city]", with: "Town")
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq("uprn_not_listed")
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq("")
expect(sales_log.town_or_city).to eq("Town")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
end
context "with changing the postcode" do
before do
fill_in("sales_log[town_or_city]", with: "Town")
fill_in("sales_log[postcode_full]", with: "AA12AA")
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq("uprn_not_listed")
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 2AA")
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq("")
expect(sales_log.town_or_city).to eq("Town")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E08000010")
end
end
end
end
context "and the user selects 'address_not_listed' and then changes their mind and selects an address" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
visit("/sales-logs/#{sales_log.id}/uprn-selection")
choose("sales-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(1)
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(1)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
end
context "and possible addresses found and selected" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose("sales-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(1)
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(1)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
end
end
end
end
context "when a log becomes a duplicate" do

4
spec/fixtures/exports/general_needs_log.xml vendored

@ -147,10 +147,10 @@
<duplicate_set_id/>
<formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2022-05-01T00:00:00+01:00</createddate>
<uploaddate>2022-05-01T00:00:00+01:00</uploaddate>

4
spec/fixtures/exports/general_needs_log_23_24.xml vendored

@ -148,10 +148,10 @@
<duplicate_set_id/>
<formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2023-04-03T00:00:00+01:00</createddate>
<uploaddate>2023-04-03T00:00:00+01:00</uploaddate>

4
spec/fixtures/exports/general_needs_log_24_25.xml vendored

@ -161,10 +161,10 @@
<la_as_entered>la as entered</la_as_entered>
<formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2024-04-03T00:00:00+01:00</createddate>
<uploaddate>2024-04-03T00:00:00+01:00</uploaddate>

2
spec/fixtures/exports/organisation.xml vendored

@ -2,7 +2,7 @@
<forms>
<form>
<id>{id}</id>
<name>MHCLG</name>
<name>{name}</name>
<phone/>
<provider_type>1</provider_type>
<address_line1>2 Marsham Street</address_line1>

4
spec/fixtures/exports/supported_housing_logs.xml vendored

@ -146,10 +146,10 @@
<duplicate_set_id/>
<formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2022-05-01T00:00:00+01:00</createddate>
<uploaddate>2022-05-01T00:00:00+01:00</uploaddate>

2
spec/fixtures/exports/user.xml vendored

@ -12,6 +12,6 @@
<is_dpo>false</is_dpo>
<is_key_contact>false</is_key_contact>
<active>true</active>
<organisation_name>MHCLG</organisation_name>
<organisation_name>{organisation_name}</organisation_name>
</form>
</forms>

2
spec/helpers/tab_nav_helper_spec.rb

@ -9,7 +9,7 @@ RSpec.describe TabNavHelper do
describe "#user_cell" do
it "returns user link and email separated by a newline character" do
expected_html = "<a class=\"govuk-link\" href=\"/users\">#{current_user.name}</a>\n<span class=\"govuk-visually-hidden\">User </span><span class=\"govuk-!-font-weight-regular app-!-colour-muted\">#{current_user.email}</span>"
expect(user_cell(current_user)).to match(expected_html)
expect(CGI.unescapeHTML(user_cell(current_user))).to match(expected_html)
end
end

6
spec/lib/tasks/data_export_spec.rb

@ -30,7 +30,7 @@ describe "rake core:data_export", type: task do
context "with all available years" do
it "calls the export service" do
expect(export_service).to receive(:export_xml).with(full_update: true, collection: nil)
expect(export_service).to receive(:export_xml).with(full_update: true, collection: nil, year: nil)
task.invoke
end
@ -38,9 +38,9 @@ describe "rake core:data_export", type: task do
context "with a specific collection" do
it "calls the export service" do
expect(export_service).to receive(:export_xml).with(full_update: true, collection: 2022)
expect(export_service).to receive(:export_xml).with(full_update: true, collection: "lettings", year: 2022)
task.invoke("2022")
task.invoke("lettings", "2022")
end
end
end

41
spec/lib/tasks/set_export_collection_years_spec.rb

@ -0,0 +1,41 @@
require "rails_helper"
require "rake"
RSpec.describe "set_export_collection_years" do
describe ":set_export_collection_years", type: :task do
subject(:task) { Rake::Task["set_export_collection_years"] }
before do
Rake.application.rake_require("tasks/set_export_collection_years")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the rake task is run" do
let!(:lettings_export_2023) { Export.create(collection: "2023", year: nil, started_at: Time.zone.now) }
let!(:lettings_export_2024) { Export.create(collection: "2024", year: nil, started_at: Time.zone.now) }
let!(:updated_lettings_export) { Export.create(collection: "lettings", year: 2023, started_at: Time.zone.now) }
let!(:organisations_export) { Export.create(collection: "organisations", year: nil, started_at: Time.zone.now) }
let!(:users_export) { Export.create(collection: "users", year: nil, started_at: Time.zone.now) }
it "correctly updates collection years" do
task.invoke
expect(lettings_export_2023.reload.collection).to eq("lettings")
expect(lettings_export_2023.year).to eq(2023)
expect(lettings_export_2024.reload.collection).to eq("lettings")
expect(lettings_export_2024.year).to eq(2024)
expect(updated_lettings_export.reload.collection).to eq("lettings")
expect(updated_lettings_export.year).to eq(2023)
expect(organisations_export.reload.collection).to eq("organisations")
expect(organisations_export.year).to eq(nil)
expect(users_export.reload.collection).to eq("users")
expect(users_export.year).to eq(nil)
end
end
end
end

25
spec/mailers/bulk_upload_mailer_spec.rb

@ -121,4 +121,29 @@ RSpec.describe BulkUploadMailer do
mailer.send_check_soft_validations_mail(bulk_upload:)
end
end
describe "#send_correct_duplicates_and_upload_again_mail" do
context "when 2 columns with errors" do
before do
create(:bulk_upload_error, bulk_upload:, col: "A")
create(:bulk_upload_error, bulk_upload:, col: "B")
end
it "sends correctly formed email" do
expect(notify_client).to receive(:send_email).with(
email_address: user.email,
template_id: described_class::FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID,
personalisation: {
filename: bulk_upload.filename,
upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time),
year_combo: bulk_upload.year_combo,
lettings_or_sales: bulk_upload.log_type,
summary_report_link: "http://localhost:3000/lettings-logs/bulk-upload-results/#{bulk_upload.id}",
},
)
mailer.send_correct_duplicates_and_upload_again_mail(bulk_upload:)
end
end
end
end

11
spec/mailers/devise_notify_mailer_spec.rb

@ -36,8 +36,11 @@ RSpec.describe DeviseNotifyMailer do
end
context "when the email domain is in the allowlist" do
let(:domain) { Rails.application.credentials[:email_allowlist].first }
let(:email) { "test@#{domain}" }
before do
allow(Rails.application.credentials).to receive(:[]).with(:email_allowlist).and_return(["example.com"])
end
let(:email) { "test@example.com" }
it "does send emails" do
expect(notify_client).to receive(:send_email).once
@ -48,10 +51,10 @@ RSpec.describe DeviseNotifyMailer do
context "when notify mailer raises BadRequestError" do
before do
allow(notify_client).to receive(:send_email).and_raise(bad_request_error)
allow(Rails.application.credentials).to receive(:[]).with(:email_allowlist).and_return(["example.com"])
end
let(:domain) { Rails.application.credentials[:email_allowlist].first }
let(:email) { "test@#{domain}" }
let(:email) { "test@example.com" }
it "does not raise an error" do
expect {

2
spec/models/form/lettings/questions/managing_organisation_spec.rb

@ -185,7 +185,7 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do
context "when organisation has merged" do
let(:absorbing_org) { create(:organisation, name: "Absorbing org", holds_own_stock: true) }
let!(:merged_org) { create(:organisation, name: "Merged org", holds_own_stock: false) }
let!(:merged_deleted_org) { create(:organisation, name: "Merged org", holds_own_stock: false, discarded_at: Time.zone.yesterday) }
let!(:merged_deleted_org) { create(:organisation, name: "Merged org 2", holds_own_stock: false, discarded_at: Time.zone.yesterday) }
let(:user) { create(:user, :data_coordinator, organisation: absorbing_org) }
let(:log) do

4
spec/models/form/sales/pages/equity_spec.rb

@ -7,6 +7,10 @@ RSpec.describe Form::Sales::Pages::Equity, type: :model do
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1))) }
before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end

4
spec/models/form/sales/pages/value_shared_ownership_spec.rb

@ -7,6 +7,10 @@ RSpec.describe Form::Sales::Pages::ValueSharedOwnership, type: :model do
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1))) }
before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end

28
spec/models/form/sales/questions/buyer2_working_situation_spec.rb

@ -36,6 +36,22 @@ RSpec.describe Form::Sales::Questions::Buyer2WorkingSituation, type: :model do
"0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" },
"7" => { "value" => "Full-time student" },
"9" => { "value" => "Child under 16" },
})
end
it "has the correct displayed_answer_options" do
expect(question.displayed_answer_options(nil)).to eq({
"1" => { "value" => "Full-time - 30 hours or more" },
"2" => { "value" => "Part-time - Less than 30 hours" },
"3" => { "value" => "In government training into work" },
"4" => { "value" => "Jobseeker" },
"6" => { "value" => "Not seeking work" },
"8" => { "value" => "Unable to work due to long term sick or disability" },
"5" => { "value" => "Retired" },
"0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" },
"7" => { "value" => "Full-time student" },
})
end
@ -43,7 +59,11 @@ RSpec.describe Form::Sales::Questions::Buyer2WorkingSituation, type: :model do
let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2025_or_later?: false) }
it "uses the old ordering for answer options" do
expect(question.answer_options.keys).to eq(%w[1 2 3 4 6 8 5 0 10 7])
expect(question.answer_options.keys).to eq(%w[1 2 3 4 6 8 5 0 10 7 9])
end
it "uses the old ordering for displayed answer options" do
expect(question.displayed_answer_options(nil).keys).to eq(%w[1 2 3 4 6 8 5 0 10 7])
end
end
@ -51,7 +71,11 @@ RSpec.describe Form::Sales::Questions::Buyer2WorkingSituation, type: :model do
let(:form) { instance_double(Form, start_date: Time.zone.local(2025, 4, 1), start_year_2025_or_later?: true) }
it "uses the new ordering for answer options" do
expect(question.answer_options.keys).to eq(%w[1 2 3 4 5 6 7 8 0 10])
expect(question.answer_options.keys).to eq(%w[1 2 3 4 5 6 7 8 9 0 10])
end
it "uses the new ordering for displayed answer options" do
expect(question.displayed_answer_options(nil).keys).to eq(%w[1 2 3 4 5 6 7 8 0 10])
end
end

4
spec/models/form/sales/questions/equity_spec.rb

@ -7,6 +7,10 @@ RSpec.describe Form::Sales::Questions::Equity, type: :model do
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, id: "initial_equity", subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)))) }
before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has correct page" do
expect(question.page).to eq(page)
end

6
spec/models/organisation_spec.rb

@ -19,6 +19,12 @@ RSpec.describe Organisation, type: :model do
.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Provider type #{I18n.t('validations.organisation.provider_type_missing')}")
end
it "validates uniqueness of name" do
org = build(:organisation, name: organisation.name.downcase)
org.valid?
expect(org.errors[:name]).to include(I18n.t("validations.organisation.name_not_unique"))
end
context "with parent/child associations", :aggregate_failures do
let!(:child_organisation) { create(:organisation, name: "MHCLG Child") }
let!(:grandchild_organisation) { create(:organisation, name: "MHCLG Grandchild") }

7
spec/models/scheme_spec.rb

@ -390,6 +390,13 @@ RSpec.describe Scheme, type: :model do
scheme.startdate = Time.zone.today + 2.weeks
expect(scheme.status).to eq(:activating_soon)
end
it "returns deactivated if scheme is deactivated and incomplete" do
scheme.update!(support_type: nil, confirmed: nil)
FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.yesterday, scheme:)
scheme.reload
expect(scheme.status).to eq(:deactivated)
end
end
context "when there have been previous deactivations" do

1
spec/models/user_spec.rb

@ -300,6 +300,7 @@ RSpec.describe User, type: :model do
context "when the user is in staging environment" do
before do
allow(Rails.env).to receive(:staging?).and_return(true)
allow(Rails.application.credentials).to receive(:[]).with(:staging_role_update_email_allowlist).and_return(["not_one_of_the_examples.com"])
end
context "and the user is not in the staging role update email allowlist" do

111
spec/requests/bulk_upload_lettings_logs_controller_spec.rb

@ -73,5 +73,116 @@ RSpec.describe BulkUploadLettingsLogsController, type: :request do
expect(response.body).to include("How to upload logs in bulk")
end
end
context "when no year is specified" do
it "shows guidance page with links defaulting to the current year" do
get "/lettings-logs/bulk-upload-logs/guidance"
expect(response.body).to include("Download the lettings bulk upload template (#{current_collection_start_year} to #{current_collection_start_year + 1})")
end
end
context "when an invalid year is specified" do
it "shows not found" do
get "/lettings-logs/bulk-upload-logs/guidance?form%5Byear%5D=10000"
expect(response).to be_not_found
end
end
end
describe "GET /lettings-logs/bulk-upload-logs/year" do
it "does not require a year to be specified" do
get "/lettings-logs/bulk-upload-logs/year"
expect(response).to be_ok
end
end
pages_requiring_year_specification = %w[prepare-your-file upload-your-file checking-file]
pages_requiring_year_specification.each do |page_id|
describe "GET /lettings-logs/bulk-upload-logs/#{page_id}" do
context "when no year is provided" do
it "returns not found" do
get "/lettings-logs/bulk-upload-logs/#{page_id}"
expect(response).to be_not_found
end
end
context "when requesting the previous year in a crossover period" do
before do
allow(FormHandler.instance).to receive(:lettings_in_crossover_period?).and_return(true)
end
it "succeeds" do
get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}"
expect(response).to be_ok
end
end
context "when requesting the previous year outside a crossover period" do
before do
allow(FormHandler.instance).to receive(:lettings_in_crossover_period?).and_return(false)
end
it "returns not found" do
get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}"
expect(response).to be_not_found
end
end
context "when requesting the current year" do
it "succeeds" do
get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year}"
expect(response).to be_ok
end
end
if page_id != "prepare-your-file"
context "when requesting the next year with future form use toggled on" do
before do
allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(true)
end
it "succeeds" do
get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}"
expect(response).to be_ok
end
end
end
context "when requesting the next year with future form use toggled off" do
before do
allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(false)
end
it "returns not found" do
get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}"
expect(response).to be_not_found
end
end
context "when requesting a far future year" do
it "returns not found" do
get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=9990"
expect(response).to be_not_found
end
end
context "when requesting a nonsense value for year" do
it "returns not found" do
get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=thisisnotayear"
expect(response).to be_not_found
end
end
end
end
end

111
spec/requests/bulk_upload_sales_logs_controller_spec.rb

@ -73,5 +73,116 @@ RSpec.describe BulkUploadSalesLogsController, type: :request do
expect(response.body).to include("How to upload logs in bulk")
end
end
context "when no year is specified" do
it "shows guidance page with links defaulting to the current year" do
get "/sales-logs/bulk-upload-logs/guidance"
expect(response.body).to include("Download the sales bulk upload template (#{current_collection_start_year} to #{current_collection_start_year + 1})")
end
end
context "when an invalid year is specified" do
it "shows not found" do
get "/sales-logs/bulk-upload-logs/guidance?form%5Byear%5D=10000"
expect(response).to be_not_found
end
end
end
describe "GET /sales-logs/bulk-upload-logs/year" do
it "does not require a year to be specified" do
get "/sales-logs/bulk-upload-logs/year"
expect(response).to be_ok
end
end
pages_requiring_year_specification = %w[prepare-your-file upload-your-file checking-file]
pages_requiring_year_specification.each do |page_id|
describe "GET /sales-logs/bulk-upload-logs/#{page_id}" do
context "when no year is provided" do
it "returns not found" do
get "/sales-logs/bulk-upload-logs/#{page_id}"
expect(response).to be_not_found
end
end
context "when requesting the previous year in a crossover period" do
before do
allow(FormHandler.instance).to receive(:sales_in_crossover_period?).and_return(true)
end
it "succeeds" do
get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}"
expect(response).to be_ok
end
end
context "when requesting the previous year outside a crossover period" do
before do
allow(FormHandler.instance).to receive(:sales_in_crossover_period?).and_return(false)
end
it "returns not found" do
get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}"
expect(response).to be_not_found
end
end
context "when requesting the current year" do
it "succeeds" do
get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year}"
expect(response).to be_ok
end
end
if page_id != "prepare-your-file"
context "when requesting the next year with future form use toggled on" do
before do
allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(true)
end
it "succeeds" do
get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}"
expect(response).to be_ok
end
end
end
context "when requesting the next year with future form use toggled off" do
before do
allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(false)
end
it "returns not found" do
get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}"
expect(response).to be_not_found
end
end
context "when requesting a far future year" do
it "returns not found" do
get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=9990"
expect(response).to be_not_found
end
end
context "when requesting a nonsense value for year" do
it "returns not found" do
get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=thisisnotayear"
expect(response).to be_not_found
end
end
end
end
end

6
spec/requests/lettings_logs_controller_spec.rb

@ -759,7 +759,7 @@ RSpec.describe LettingsLogsController, type: :request do
it "has search results in the title" do
get "/lettings-logs?search=#{log_to_search.id}", headers:, params: {}
expect(page).to have_title("Lettings logs (1 logs matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK")
expect(page).to have_title("Lettings logs (1 log matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
it "shows lettings logs matching the id" do
@ -895,7 +895,7 @@ RSpec.describe LettingsLogsController, type: :request do
end
it "shows the total log count" do
expect(CGI.unescape_html(response.body)).to match("<strong>1</strong> total logs")
expect(CGI.unescape_html(response.body)).to match("<strong>1</strong> total log")
end
it "does not show the pagination links" do
@ -1483,7 +1483,7 @@ RSpec.describe LettingsLogsController, type: :request do
end
context "when viewing a collection of logs affected by deactivated location" do
let!(:affected_lettings_logs) { FactoryBot.create_list(:lettings_log, 3, unresolved: true, assigned_to: user) }
let!(:affected_lettings_logs) { FactoryBot.create_list(:lettings_log, 3, unresolved: true, assigned_to: user, tenancycode: "affected tenancycode", propcode: "affected propcode") }
let!(:other_user_affected_lettings_log) { FactoryBot.create(:lettings_log, unresolved: true) }
let!(:non_affected_lettings_logs) { FactoryBot.create_list(:lettings_log, 4, assigned_to: user) }
let(:other_user) { FactoryBot.create(:user, organisation: user.organisation) }

28
spec/requests/organisation_relationships_controller_spec.rb

@ -40,7 +40,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
it "shows a table of stock owners" do
expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html)
expect(response.body).to include(stock_owner.name)
expect(CGI.unescapeHTML(response.body)).to include(stock_owner.name)
end
it "shows only stock owners for the current user's organisation" do
@ -53,12 +53,12 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end
it "shows the pagination count" do
expect(page).to have_content("1 total stock owners")
expect(page).to have_content("1 total stock owner")
end
context "when adding a stock owner" do
let!(:active_organisation) { FactoryBot.create(:organisation, name: "Active Org", active: true) }
let!(:inactive_organisation) { FactoryBot.create(:organisation, name: "Inactive LTD", active: false) }
let!(:inactive_organisation) { FactoryBot.create(:organisation, name: "Inactive LTD 2", active: false) }
before do
get "/organisations/#{organisation.id}/stock-owners/add", headers:, params: {}
@ -115,7 +115,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
let!(:managing_agent) { FactoryBot.create(:organisation) }
let!(:other_org_managing_agent) { FactoryBot.create(:organisation, name: "Foobar LTD") }
let!(:inactive_managing_agent) { FactoryBot.create(:organisation, name: "Inactive LTD", active: false) }
let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") }
let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 3") }
before do
FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent)
@ -136,7 +136,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
it "shows a table of managing-agents" do
expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html)
expect(response.body).to include(managing_agent.name)
expect(CGI.unescapeHTML(response.body)).to include(managing_agent.name)
end
it "shows only managing-agents for the current user's organisation" do
@ -149,7 +149,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end
it "shows the pagination count" do
expect(page).to have_content("1 total managing agents")
expect(page).to have_content("1 total managing agent")
end
context "and current organisation is deactivated" do
@ -316,7 +316,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
context "with an organisation that the user belongs to" do
let!(:stock_owner) { FactoryBot.create(:organisation) }
let!(:other_org_stock_owner) { FactoryBot.create(:organisation, name: "Foobar LTD") }
let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") }
let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 2") }
before do
FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: stock_owner)
@ -336,7 +336,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
it "shows a table of stock owners" do
expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html)
expect(response.body).to include(stock_owner.name)
expect(CGI.unescapeHTML(response.body)).to include(stock_owner.name)
end
it "shows only stock owners for the current user's organisation" do
@ -345,7 +345,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end
it "shows the pagination count" do
expect(page).to have_content("1 total stock owners")
expect(page).to have_content("1 total stock owner")
end
end
@ -452,7 +452,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
context "with an organisation that the user belongs to" do
let!(:managing_agent) { FactoryBot.create(:organisation) }
let!(:other_org_managing_agent) { FactoryBot.create(:organisation, name: "Foobar LTD") }
let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") }
let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 5") }
before do
FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent)
@ -472,7 +472,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
it "shows a table of managing agents" do
expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html)
expect(response.body).to include(managing_agent.name)
expect(CGI.unescapeHTML(response.body)).to include(managing_agent.name)
end
it "shows only managing agents for the current user's organisation" do
@ -481,7 +481,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end
it "shows the pagination count" do
expect(page).to have_content("1 total managing agents")
expect(page).to have_content("1 total managing agent")
end
end
@ -647,7 +647,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end
it "shows the pagination count" do
expect(page).to have_content("1 total stock owners")
expect(page).to have_content("1 total stock owner")
end
context "when adding a stock owner" do
@ -697,7 +697,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end
it "shows the pagination count" do
expect(page).to have_content("1 total managing agents")
expect(page).to have_content("1 total managing agent")
end
it "shows remove link(s)" do

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

Loading…
Cancel
Save