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

6
.github/workflows/review_teardown_pipeline.yml

@ -25,13 +25,13 @@ jobs:
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3 uses: aws-actions/configure-aws-credentials@v4
with: with:
aws-region: ${{ env.aws_region }} aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }} role-to-assume: ${{ env.app_repo_role }}
- name: Configure AWS credentials for review environment - name: Configure AWS credentials for review environment
uses: aws-actions/configure-aws-credentials@v3 uses: aws-actions/configure-aws-credentials@v4
with: with:
aws-region: ${{ env.aws_region }} aws-region: ${{ env.aws_region }}
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/${{ env.aws_role_prefix }}-deployment 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) network=$(aws ecs describe-services --cluster $cluster --services $service --query services[0].networkConfiguration)
overrides='{ "containerOverrides" : [{ "name" : "app", "command" : ["bundle", "exec", "rake", "db:drop"]}]}' 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) 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##*/} temp=${arn##*/}
id=${temp%*\"} id=${temp%*\"}
aws ecs wait tasks-stopped --cluster $cluster --tasks $id 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: push:
branches: branches:
- main - main
pull_request:
types:
- opened
- synchronize
merge_group:
workflow_dispatch: workflow_dispatch:
defaults: defaults:
@ -21,347 +16,13 @@ env:
repository: core repository: core
jobs: jobs:
test: tests:
name: Tests name: Run Tests
runs-on: ubuntu-latest uses: ./.github/workflows/run_tests.yml
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
aws_deploy: aws_deploy:
name: AWS Deploy name: AWS Deploy
if: github.ref == 'refs/heads/main' needs: [tests]
needs: [lint, test, feature_test, requests_test, model_test, audit]
uses: ./.github/workflows/aws_deploy.yml uses: ./.github/workflows/aws_deploy.yml
with: with:
aws_account_id: 107155005276 aws_account_id: 107155005276
@ -379,13 +40,13 @@ jobs:
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3 uses: aws-actions/configure-aws-credentials@v4
with: with:
aws-region: ${{ env.aws_region }} aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }} role-to-assume: ${{ env.app_repo_role }}
- name: Configure AWS credentials for the environment - name: Configure AWS credentials for the environment
uses: aws-actions/configure-aws-credentials@v3 uses: aws-actions/configure-aws-credentials@v4
with: with:
aws-region: eu-west-2 aws-region: eu-west-2
role-to-assume: arn:aws:iam::107155005276:role/core-staging-deployment 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" ruby "3.1.4"
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem "rails", "~> 7.0.8.5" gem "rails", "~> 7.0.8.7"
# Use postgresql as the database for Active Record # Use postgresql as the database for Active Record
gem "pg", "~> 1.1" gem "pg", "~> 1.1"
# Use Puma as the app server # Use Puma as the app server

130
Gemfile.lock

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

23
app/controllers/bulk_upload_lettings_logs_controller.rb

@ -1,6 +1,7 @@
class BulkUploadLettingsLogsController < ApplicationController class BulkUploadLettingsLogsController < ApplicationController
before_action :authenticate_user! 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 def start
if have_choice_of_year? if have_choice_of_year?
@ -24,12 +25,26 @@ class BulkUploadLettingsLogsController < ApplicationController
private private
def validate_data_protection_agrement_signed! def validate_data_protection_agreement_signed!
return if @current_user.organisation.data_protection_confirmed? return if @current_user.organisation.data_protection_confirmed?
redirect_to lettings_logs_path redirect_to lettings_logs_path
end 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 def current_year
FormHandler.instance.current_collection_start_year FormHandler.instance.current_collection_start_year
end end
@ -48,8 +63,6 @@ private
Forms::BulkUploadLettings::PrepareYourFile.new(form_params) Forms::BulkUploadLettings::PrepareYourFile.new(form_params)
when "guidance" when "guidance"
Forms::BulkUploadLettings::Guidance.new(form_params.merge(referrer: params[:referrer])) Forms::BulkUploadLettings::Guidance.new(form_params.merge(referrer: params[:referrer]))
when "needstype"
Forms::BulkUploadLettings::Needstype.new(form_params)
when "upload-your-file" when "upload-your-file"
Forms::BulkUploadLettings::UploadYourFile.new(form_params.merge(current_user:)) Forms::BulkUploadLettings::UploadYourFile.new(form_params.merge(current_user:))
when "checking-file" when "checking-file"
@ -60,6 +73,6 @@ private
end end
def form_params def form_params
params.fetch(:form, {}).permit(:year, :needstype, :file, :organisation_id) params.fetch(:form, {}).permit(:year, :file, :organisation_id)
end end
end end

19
app/controllers/bulk_upload_sales_logs_controller.rb

@ -1,6 +1,7 @@
class BulkUploadSalesLogsController < ApplicationController class BulkUploadSalesLogsController < ApplicationController
before_action :authenticate_user! 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 def start
if have_choice_of_year? if have_choice_of_year?
@ -24,12 +25,26 @@ class BulkUploadSalesLogsController < ApplicationController
private private
def validate_data_protection_agrement_signed! def validate_data_protection_agreement_signed!
return if @current_user.organisation.data_protection_confirmed? return if @current_user.organisation.data_protection_confirmed?
redirect_to sales_logs_path redirect_to sales_logs_path
end 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 def current_year
FormHandler.instance.current_collection_start_year FormHandler.instance.current_collection_start_year
end end

1
app/controllers/organisations_controller.rb

@ -155,6 +155,7 @@ class OrganisationsController < ApplicationController
end end
redirect_to details_organisation_path(@organisation) redirect_to details_organisation_path(@organisation)
else 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) @rent_periods = helpers.rent_periods_with_checked_attr(checked_periods: selected_rent_periods)
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
end 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" @scheme.update!(secondary_client_group: nil) if @scheme.has_other_client_group == "No"
if scheme_params[:confirmed] == "true" || @scheme.confirmed? if scheme_params[:confirmed] == "true" || @scheme.confirmed?
if check_answers && should_direct_via_secondary_client_group_page?(page) 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 else
@scheme.locations.update!(confirmed: true) @scheme.locations.update!(confirmed: true)
flash[:notice] = if scheme_previously_confirmed flash[:notice] = if scheme_previously_confirmed
@ -162,7 +162,7 @@ class SchemesController < ApplicationController
end end
elsif check_answers elsif check_answers
if should_direct_via_secondary_client_group_page?(page) 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 else
redirect_to scheme_check_answers_path(@scheme) redirect_to scheme_check_answers_path(@scheme)
end end

18
app/helpers/schemes_helper.rb

@ -131,6 +131,24 @@ module SchemesHelper
messages[attribute[:id]] || "Enter #{text}" messages[attribute[:id]] || "Enter #{text}"
end 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 private
ActivePeriod = Struct.new(:from, :to) 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 COMPLETE_TEMPLATE_ID = "83279578-c890-4168-838b-33c9cf0dc9f0".freeze
FAILED_CSV_ERRORS_TEMPLATE_ID = "e27abcd4-5295-48c2-b127-e9ee4b781b75".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_FILE_SETUP_ERROR_TEMPLATE_ID = "24c9f4c7-96ad-470a-ba31-eb51b7cbafd9".freeze
FAILED_SERVICE_ERROR_TEMPLATE_ID = "c3f6288c-7a74-4e77-99ee-6c4a0f6e125a".freeze FAILED_SERVICE_ERROR_TEMPLATE_ID = "c3f6288c-7a74-4e77-99ee-6c4a0f6e125a".freeze
HOW_TO_FIX_UPLOAD_TEMPLATE_ID = "21a07b26-f625-4846-9f4d-39e30937aa24".freeze HOW_TO_FIX_UPLOAD_TEMPLATE_ID = "21a07b26-f625-4846-9f4d-39e30937aa24".freeze
@ -95,6 +96,26 @@ class BulkUploadMailer < NotifyMailer
) )
end 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:) def send_bulk_upload_failed_file_setup_error_mail(bulk_upload:)
bulk_upload_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors? 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) 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? if uprn_known&.zero?
self.uprn = nil 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 end
if uprn_known == 1 && uprn_confirmed&.zero? 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? if uprn_known&.zero?
self.uprn = nil 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 end
if uprn_known == 1 && uprn_confirmed&.zero? if uprn_known == 1 && uprn_confirmed&.zero?

3
app/models/export.rb

@ -1,2 +1,5 @@
class Export < ApplicationRecord class Export < ApplicationRecord
scope :lettings, -> { where(collection: "lettings") }
scope :organisations, -> { where(collection: "organisations") }
scope :users, -> { where(collection: "users") }
end 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] @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end end
def displayed_answer_options(_log, _user = nil)
answer_options.reject { |key, _| key == "9" }
end
def answer_options def answer_options
if form.start_year_2025_or_later? if form.start_year_2025_or_later?
{ {
@ -26,6 +30,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
"6" => { "value" => "Not seeking work" }, "6" => { "value" => "Not seeking work" },
"7" => { "value" => "Full-time student" }, "7" => { "value" => "Full-time student" },
"8" => { "value" => "Unable to work due to long term sick or disability" }, "8" => { "value" => "Unable to work due to long term sick or disability" },
"9" => { "value" => "Child under 16" },
"0" => { "value" => "Other" }, "0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" }, "10" => { "value" => "Buyer prefers not to say" },
}.freeze }.freeze
@ -41,6 +46,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
"0" => { "value" => "Other" }, "0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" }, "10" => { "value" => "Buyer prefers not to say" },
"7" => { "value" => "Full-time student" }, "7" => { "value" => "Full-time student" },
"9" => { "value" => "Child under 16" },
}.freeze }.freeze
end end
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) def initialize(id, hsh, page)
super super
@id = "equity" @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" @type = "numeric"
@min = 0 @min = 0
@max = 100 @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) def initialize(id, hsh, page)
super super
@id = "value" @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" @type = "numeric"
@min = 0 @min = 0
@step = 1 @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 end
def address_options 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?) if [address_line1_input, postcode_full_input].all?(&:present?)
@last_searched_address_string = address_string
service = AddressClient.new(address_string) service = AddressClient.new(address_string)
service.call service.call
if service.result.blank? || service.error.present? if service.result.blank? || service.error.present?

1
app/models/organisation.rb

@ -58,6 +58,7 @@ class Organisation < ApplicationRecord
alias_method :la?, :LA? alias_method :la?, :LA?
validates :name, presence: { message: I18n.t("validations.organisation.name_missing") } 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") } validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_missing") }
def self.find_by_id_on_multiple_fields(id) 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) def status_at(date)
return :deleted if discarded_at.present? 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 || 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) (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 :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 :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 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? 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')})" 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}%)" : "" 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", record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.discounted_ownership_value",
mortgage: record.mortgage&.positive? ? " (#{record.field_formatted_as_currency('mortgage')})" : "", mortgage: record.mortgage&.positive? ? " (#{record.field_formatted_as_currency('mortgage')})" : "",
deposit_and_grant_sentence:, deposit_and_grant_sentence:,
@ -72,6 +72,12 @@ module Validations::Sales::SaleInformationValidations
discount_sentence:, discount_sentence:,
value_with_discount: record.field_formatted_as_currency("value_with_discount")).html_safe value_with_discount: record.field_formatted_as_currency("value_with_discount")).html_safe
end 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
end end

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

@ -40,29 +40,25 @@ class BulkUpload::Lettings::Validator
end end
end end
def create_logs? def block_log_creation_reason
return false if any_setup_errors? return "setup_errors" if any_setup_errors?
if row_parsers.any?(&:block_log_creation?) if row_parsers.any?(&:block_log_creation?)
Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.") Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.")
return false return "row_parser_block_log_creation"
end end
if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled? 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 "duplicate_logs"
return false
end end
row_parsers.each do |row_parser| row_parsers.each do |row_parser|
row_parser.log.blank_invalid_non_setup_fields! row_parser.log.blank_invalid_non_setup_fields!
end end
if any_logs_invalid? if any_logs_invalid?
Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.") 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 end
true
end end
def self.question_for_field(field) 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 def validate_uprn_exists_if_any_key_address_fields_are_blank
if field_16.blank? && !key_address_fields_provided? 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
end end
def validate_address_option_found def validate_address_option_found
if log.uprn.nil? && field_16.blank? && key_address_fields_provided? 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| %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 end
end end
@ -625,19 +633,19 @@ private
def validate_address_fields def validate_address_fields
if field_16.blank? || log.errors.attribute_names.include?(:uprn) 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.")) errors.add(:field_17, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1."))
end 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.")) errors.add(:field_19, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city."))
end 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.")) errors.add(:field_21, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode."))
end 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.")) errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))
end end
end end

40
app/services/bulk_upload/processor.rb

@ -36,20 +36,28 @@ class BulkUpload::Processor
if validator.any_setup_errors? if validator.any_setup_errors?
send_setup_errors_mail 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 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 end
rescue StandardError => e rescue StandardError => e
Sentry.capture_exception(e) Sentry.capture_exception(e)
@ -97,6 +105,12 @@ private
.deliver_later .deliver_later
end 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 def send_success_mail
BulkUploadMailer BulkUploadMailer
.send_bulk_upload_complete_mail(user:, bulk_upload:) .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
end end
def create_logs? def block_log_creation_reason
return false if any_setup_errors? return "setup_errors" if any_setup_errors?
if row_parsers.any?(&:block_log_creation?) if row_parsers.any?(&:block_log_creation?)
Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.") Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.")
return false return "row_parser_block_log_creation"
end end
if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled? 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}.") Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.")
return false return "duplicate_logs"
end end
row_parsers.each do |row_parser| row_parsers.each do |row_parser|
@ -58,10 +58,8 @@ class BulkUpload::Sales::Validator
if any_logs_invalid? if any_logs_invalid?
Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.") 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 end
true
end end
def any_setup_errors? 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 def validate_uprn_exists_if_any_key_address_fields_are_blank
if field_22.blank? && !key_address_fields_provided? 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
end end
def validate_address_option_found def validate_address_option_found
if log.uprn.nil? && field_22.blank? && key_address_fields_provided? 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| %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 end
end end
@ -623,19 +631,19 @@ private
def validate_address_fields def validate_address_fields
if field_22.blank? || log.errors.attribute_names.include?(:uprn) 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.")) errors.add(:field_23, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1."))
end 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.")) errors.add(:field_25, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city."))
end 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.")) errors.add(:field_27, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode."))
end 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.")) errors.add(:field_28, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))
end end
end end

10
app/services/exports/export_service.rb

@ -7,7 +7,7 @@ module Exports
@logger = logger @logger = logger
end end
def export_xml(full_update: false, collection: nil) def export_xml(full_update: false, collection: nil, year: nil)
start_time = Time.zone.now start_time = Time.zone.now
daily_run_number = get_daily_run_number daily_run_number = get_daily_run_number
lettings_archives_for_manifest = {} lettings_archives_for_manifest = {}
@ -21,12 +21,12 @@ module Exports
when "organisations" when "organisations"
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update) organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
else 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 end
else else
users_archives_for_manifest = get_user_archives(start_time, full_update) users_archives_for_manifest = get_user_archives(start_time, full_update)
organisations_archives_for_manifest = get_organisation_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 end
write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest).merge(organisations_archives_for_manifest)) 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:) organisations_export_service.export_xml_organisations(full_update:)
end 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 = 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 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) def export_xml_lettings_logs(full_update: false, collection_year: nil)
archives_for_manifest = {} archives_for_manifest = {}
collection_years_to_export(collection_year).each do |collection| collection_years_to_export(collection_year).each do |year|
recent_export = Export.where(collection:).order("started_at").last recent_export = Export.lettings.where(year:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1 base_number = Export.lettings.where(empty_export: false, year:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update) export = build_export_run("lettings", base_number, full_update, year)
archives = write_export_archive(export, collection, recent_export, full_update) archives = write_export_archive(export, year, recent_export, full_update)
archives_for_manifest.merge!(archives) archives_for_manifest.merge!(archives)
@ -22,21 +22,21 @@ module Exports
private private
def get_archive_name(collection, base_number, increment) def get_archive_name(year, base_number, increment)
return unless collection return unless year
base_number_str = "f#{base_number.to_s.rjust(4, '0')}" base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.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 end
def retrieve_resources(recent_export, full_update, collection) def retrieve_resources(recent_export, full_update, year)
if !full_update && recent_export if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time } 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 else
params = { to: @start_time } 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
end end

12
app/services/exports/organisation_export_service.rb

@ -5,9 +5,9 @@ module Exports
def export_xml_organisations(full_update: false) def export_xml_organisations(full_update: false)
collection = "organisations" 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) export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update) archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
@ -19,15 +19,13 @@ module Exports
private private
def get_archive_name(collection, base_number, increment) def get_archive_name(_year, base_number, increment)
return unless collection
base_number_str = "f#{base_number.to_s.rjust(4, '0')}" base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.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 end
def retrieve_resources(recent_export, full_update, _collection) def retrieve_resources(recent_export, full_update, _year)
if !full_update && recent_export if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time } params = { from: recent_export.started_at, to: @start_time }
Organisation.where("(updated_at >= :from AND updated_at <= :to)", params) 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) def export_xml_users(full_update: false)
collection = "users" 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) export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update) archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
@ -19,15 +19,13 @@ module Exports
private private
def get_archive_name(collection, base_number, increment) def get_archive_name(_year, base_number, increment)
return unless collection
base_number_str = "f#{base_number.to_s.rjust(4, '0')}" base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.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 end
def retrieve_resources(recent_export, full_update, _collection) def retrieve_resources(recent_export, full_update, _year)
if !full_update && recent_export if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time } params = { from: recent_export.started_at, to: @start_time }
User.where("(updated_at >= :from AND updated_at <= :to)", params) 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 private
def build_export_run(collection, base_number, full_update) def build_export_run(collection, base_number, full_update, year = nil)
@logger.info("Building export run for #{collection}") @logger.info("Building export run for #{[collection, year].join(' ')}")
previous_exports_with_data = Export.where(collection:, empty_export: false) previous_exports_with_data = Export.where(collection:, year:, empty_export: false)
increment_number = previous_exports_with_data.where(base_number:).maximum(:increment_number) || 1 increment_number = previous_exports_with_data.where(base_number:).maximum(:increment_number) || 1
@ -25,16 +25,16 @@ module Exports
end end
if previous_exports_with_data.empty? 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 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 end
def write_export_archive(export, collection, recent_export, full_update) def write_export_archive(export, year, 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 (?) 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") @logger.info("Creating #{archive} - #{initial_count} resources")
return {} if initial_count.zero? return {} if initial_count.zero?
@ -46,12 +46,12 @@ module Exports
loop do loop do
slice = if last_processed_marker.present? 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) .where("created_at > ?", last_processed_marker)
.order(:created_at) .order(:created_at)
.limit(MAX_XML_RECORDS).to_a .limit(MAX_XML_RECORDS).to_a
else else
retrieve_resources(recent_export, full_update, collection) retrieve_resources(recent_export, full_update, year)
.order(:created_at) .order(:created_at)
.limit(MAX_XML_RECORDS).to_a .limit(MAX_XML_RECORDS).to_a
end 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) %> <% title = format_title(@searched, bulk_upload_title(controller.controller_name), current_user, item_label, @pagy.count, nil) %>
<% content_for :title, title %> <% content_for :title, title %>

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

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

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

@ -1,7 +1,7 @@
<h2 class="govuk-body"> <h2 class="govuk-body">
<div class="govuk-grid-row app-search__caption"> <div class="govuk-grid-row app-search__caption">
<div class="govuk-grid-column-three-quarters"> <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? %> <% if logs&.any? %>
<%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> <%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %>
<% if @current_user.support? %> <% 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) %> <% title = format_title(@searched, "#{log_type_for_controller(controller).capitalize} logs", current_user, item_label, @pagy.count, nil) %>
<% content_for :title, title %> <% 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" %> <% title = "Update logs" %>
<% content_for :title, title %> <% 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? %> <% if current_user.support? %>
<%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %> <%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %>
@ -44,7 +44,7 @@
pagy: @pagy, pagy: @pagy,
searched: @searched, searched: @searched,
item_label:, item_label:,
search_item: "managing agents", search_item: "managing agent",
total_count: @total_count, total_count: @total_count,
remove_path: ->(org_id) { managing_agents_remove_organisation_path(target_organisation_id: org_id) }, 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? %> <% if current_user.support? %>
<%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %> <%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %>
<%= render SubNavigationComponent.new(items: secondary_items(request.path, @organisation.id)) %> <%= render SubNavigationComponent.new(items: secondary_items(request.path, @organisation.id)) %>
@ -41,7 +41,7 @@
pagy: @pagy, pagy: @pagy,
searched: @searched, searched: @searched,
item_label:, item_label:,
search_item: "stock owners", search_item: "stock owner",
total_count: @total_count, total_count: @total_count,
remove_path: ->(org_id) { stock_owners_remove_organisation_path(target_organisation_id: org_id) }, 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 %>"> <section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= 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 %> <% end %>
<%= table.with_head do |head| %> <%= table.with_head do |head| %>
<%= head.with_row do |row| %> <%= 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) %> <% title = format_title(@searched, "Organisations", current_user, item_label, @pagy.count, nil) %>
<% content_for :title, title %> <% 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) %> <% title = format_title(@searched, action_name.humanize, current_user, item_label, @pagy.count, @organisation.name) %>
<% content_for :title, title %> <% content_for :title, title %>

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

@ -2,7 +2,7 @@
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<span class="app-search__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? %> <% 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 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" %> <%= 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 :title, "Does this scheme provide for another client group?" %>
<% content_for :before_content do %> <% content_for :before_content do %>
<%= govuk_back_link(href: :back) %> <%= govuk_back_link(href: scheme_back_button_path(@scheme, "confirm_secondary_client_group")) %>
<% end %> <% end %>
<%= render partial: "organisations/headings", locals: { main: "Does this scheme provide for another client group?", sub: @scheme.service_name } %> <%= 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 :title, "Create a new supported housing scheme" %>
<% content_for :before_content do %> <% content_for :before_content do %>
<%= govuk_back_link(href: :back) %> <%= govuk_back_link(href: scheme_back_button_path(@scheme, "details")) %>
<% end %> <% end %>
<%= form_for(@scheme, method: :patch) do |f| %> <%= 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?" %> <% 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 %> <% 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 %> <% end %>
<%= form_for(@scheme, method: :patch) do |f| %> <%= 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 :title, "What is the other client group?" %>
<% content_for :before_content do %> <% content_for :before_content do %>
<%= govuk_back_link(href: :back) %> <%= govuk_back_link(href: scheme_back_button_path(@scheme, "secondary_client_group")) %>
<% end %> <% end %>
<%= form_for(@scheme, method: :patch) do |f| %> <%= 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 :title, "What support does this scheme provide?" %>
<% content_for :before_content do %> <% content_for :before_content do %>
<%= govuk_back_link(href: :back) %> <%= govuk_back_link(href: scheme_back_button_path(@scheme, "support")) %>
<% end %> <% end %>
<%= form_for(@scheme, method: :patch) do |f| %> <%= 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 %>"> <section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %> <%= govuk_table do |table| %>
<%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= 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? %> <% if current_user.support? %>
<% query = searched.present? ? "?search=#{searched}" : nil %> <% query = searched.present? ? "?search=#{searched}" : nil %>
<%= govuk_link_to "Download (CSV)", "#{request.path}.csv#{query}", type: "text/csv", style: "white-space: nowrap" %> <%= 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: organisation:
data_sharing_agreement_not_signed: "Your organisation must accept the Data Sharing Agreement before you can create any logs." 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_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." provider_type_missing: "Select the organisation type."
stock_owner: stock_owner:
blank: "You must choose a 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: age1_known:
check_answer_label: "" check_answer_label: ""
check_answer_prompt: "Answer if you know the lead tenant's age" 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?" question_text: "Do you know the lead tenant’s age?"
age1: age1:
check_answer_label: "Lead tenant’s age" 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?" question_text: "What is buyer 2's relationship to buyer 1?"
person: person:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age2:
buyer: buyer:
@ -163,7 +163,7 @@ en:
check_answer_label: "Person 2’s gender identity" check_answer_label: "Person 2’s gender identity"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: ethnic_group2:
page_header: "" page_header: ""
@ -223,7 +223,7 @@ en:
check_answer_label: "Person 2’s working situation" check_answer_label: "Person 2’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: buy2livein:
page_header: "" page_header: ""
@ -262,10 +262,10 @@ en:
relat3: relat3:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age3:
page_header: "" page_header: ""
@ -285,14 +285,14 @@ en:
check_answer_label: "Person 3’s gender identity" check_answer_label: "Person 3’s gender identity"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: ecstat3:
page_header: "" page_header: ""
check_answer_label: "Person 3’s working situation" check_answer_label: "Person 3’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_4:
page_header: "" page_header: ""
@ -303,10 +303,10 @@ en:
relat4: relat4:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age4:
page_header: "" page_header: ""
@ -326,14 +326,14 @@ en:
check_answer_label: "Person 4’s gender identity" check_answer_label: "Person 4’s gender identity"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: ecstat4:
page_header: "" page_header: ""
check_answer_label: "Person 4’s working situation" check_answer_label: "Person 4’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_5:
page_header: "" page_header: ""
@ -344,10 +344,10 @@ en:
relat5: relat5:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age5:
page_header: "" page_header: ""
@ -367,14 +367,14 @@ en:
check_answer_label: "Person 5’s gender identity" check_answer_label: "Person 5’s gender identity"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: ecstat5:
page_header: "" page_header: ""
check_answer_label: "Person 5’s working situation" check_answer_label: "Person 5’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_6:
page_header: "" page_header: ""
@ -385,10 +385,10 @@ en:
relat6: relat6:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age6:
page_header: "" page_header: ""
@ -408,11 +408,11 @@ en:
check_answer_label: "Person 6’s gender identity" check_answer_label: "Person 6’s gender identity"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: ecstat6:
page_header: "" page_header: ""
check_answer_label: "Person 6’s working situation" check_answer_label: "Person 6’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: joint_purchase:
page_header: "" page_header: ""
check_answer_label: "Housing-related benefits buyers received before buying this property" check_answer_label: "Housing-related benefits buyers received before buying this property"
check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?" question_text: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?"
not_joint_purchase: not_joint_purchase:
page_header: "" page_header: ""
check_answer_label: "Housing-related benefits buyer received before buying this property" check_answer_label: "Housing-related benefits buyer received before buying this property"
check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Was the buyer receiving any of these housing-related benefits immediately before buying this property?" question_text: "Was the buyer receiving any of these housing-related benefits immediately before buying this property?"
@ -61,10 +59,10 @@ en:
joint_purchase: joint_purchase:
page_header: "" page_header: ""
savingsnk: savingsnk:
check_answer_label: "Buyers’ total savings known?" check_answer_label: "Buyers’ total savings known"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: savings:
check_answer_label: "Buyers’ total savings before any deposit paid" check_answer_label: "Buyers’ total savings before any deposit paid"
check_answer_prompt: "" check_answer_prompt: ""
@ -73,7 +71,7 @@ en:
not_joint_purchase: not_joint_purchase:
page_header: "" page_header: ""
savingsnk: savingsnk:
check_answer_label: "Buyer’s total savings known?" check_answer_label: "Buyer’s total savings known"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Do you know how much the buyer had in savings before they paid any deposit for the property?" 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: "" page_header: ""
check_answer_label: "Staircasing transaction" check_answer_label: "Staircasing transaction"
check_answer_prompt: "" 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?" question_text: "Is this a staircasing transaction?"
about_staircasing: about_staircasing:
page_header: "About the staircasing transaction" page_header: "About the staircasing transaction"
@ -86,7 +86,6 @@ en:
joint_purchase: joint_purchase:
page_header: "" page_header: ""
check_answer_label: "Any buyers were registered providers, housing association or local authority tenants immediately before this sale?" check_answer_label: "Any buyers were registered providers, housing association or local authority tenants immediately before this sale?"
check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Were any of the buyers private registered providers, housing association or local authority tenants immediately before this sale?" question_text: "Were any of the buyers private registered providers, housing association or local authority tenants immediately before this sale?"
not_joint_purchase: 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?" question_text: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
frombeds: frombeds:
page_header: "About the buyers’ previous property" page_header: ""
check_answer_label: "Number of bedrooms in previous property" check_answer_label: "Number of bedrooms in previous property"
check_answer_prompt: "" 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?" question_text: "How many bedrooms did the property have?"
fromprop: fromprop:
@ -132,14 +131,14 @@ en:
question_text: "What was the initial percentage equity stake purchased?" question_text: "What was the initial percentage equity stake purchased?"
mortgageused: mortgageused:
page_header: "Mortgage Amount" page_header: ""
check_answer_label: "Mortgage used" check_answer_label: "Mortgage used"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Was a mortgage used for the purchase of this property?" question_text: "Was a mortgage used for the purchase of this property?"
mortgage: mortgage:
page_header: "Mortgage Amount" page_header: ""
check_answer_label: "Mortgage amount" check_answer_label: "Mortgage amount"
check_answer_prompt: "" 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." 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: age1_known:
check_answer_label: "" check_answer_label: ""
check_answer_prompt: "Answer if you know the lead tenant's age" 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?" question_text: "Do you know the lead tenant’s age?"
age1: age1:
check_answer_label: "Lead tenant’s age" 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?" question_text: "What is buyer 2's relationship to buyer 1?"
person: person:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age2:
buyer: buyer:
@ -142,7 +142,7 @@ en:
check_answer_label: "Person 2’s gender identity" check_answer_label: "Person 2’s gender identity"
check_answer_prompt: "" 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." 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: ethnic_group2:
page_header: "" page_header: ""
@ -209,7 +209,7 @@ en:
check_answer_label: "Person 2’s working situation" check_answer_label: "Person 2’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: buy2livein:
page_header: "" page_header: ""
@ -248,10 +248,10 @@ en:
relat3: relat3:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age3:
page_header: "" page_header: ""
@ -271,14 +271,14 @@ en:
check_answer_label: "Person 3’s gender identity" check_answer_label: "Person 3’s gender identity"
check_answer_prompt: "" 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." 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: ecstat3:
page_header: "" page_header: ""
check_answer_label: "Person 3’s working situation" check_answer_label: "Person 3’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_4:
page_header: "" page_header: ""
@ -289,10 +289,10 @@ en:
relat4: relat4:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age4:
page_header: "" page_header: ""
@ -312,14 +312,14 @@ en:
check_answer_label: "Person 4’s gender identity" check_answer_label: "Person 4’s gender identity"
check_answer_prompt: "" 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." 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: ecstat4:
page_header: "" page_header: ""
check_answer_label: "Person 4’s working situation" check_answer_label: "Person 4’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_5:
page_header: "" page_header: ""
@ -330,10 +330,10 @@ en:
relat5: relat5:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age5:
page_header: "" page_header: ""
@ -353,14 +353,14 @@ en:
check_answer_label: "Person 5’s gender identity" check_answer_label: "Person 5’s gender identity"
check_answer_prompt: "" 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." 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: ecstat5:
page_header: "" page_header: ""
check_answer_label: "Person 5’s working situation" check_answer_label: "Person 5’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_6:
page_header: "" page_header: ""
@ -371,10 +371,10 @@ en:
relat6: relat6:
page_header: "" 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: "" check_answer_prompt: ""
hint_text: "" 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: age6:
page_header: "" page_header: ""
@ -394,11 +394,11 @@ en:
check_answer_label: "Person 6’s gender identity" check_answer_label: "Person 6’s gender identity"
check_answer_prompt: "" 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." 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: ecstat6:
page_header: "" page_header: ""
check_answer_label: "Person 6’s working situation" check_answer_label: "Person 6’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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_label: "Buyers’ total savings known?"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: savings:
check_answer_label: "Buyers’ total savings before any deposit paid" check_answer_label: "Buyers’ total savings before any deposit paid"
check_answer_prompt: "" check_answer_prompt: ""

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

@ -33,7 +33,7 @@ en:
page_header: "" page_header: ""
check_answer_label: "Staircasing transaction" check_answer_label: "Staircasing transaction"
check_answer_prompt: "" 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?" question_text: "Is this a staircasing transaction?"
about_staircasing: about_staircasing:
page_header: "About the staircasing transaction" 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?" question_text: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
frombeds: frombeds:
page_header: "About the buyers’ previous property" page_header: ""
check_answer_label: "Number of bedrooms in previous property" check_answer_label: "Number of bedrooms in previous property"
check_answer_prompt: "" 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?" question_text: "How many bedrooms did the property have?"
fromprop: fromprop:
@ -137,14 +137,14 @@ en:
question_text: "What was the initial percentage equity stake purchased?" question_text: "What was the initial percentage equity stake purchased?"
mortgageused: mortgageused:
page_header: "Mortgage Amount" page_header: ""
check_answer_label: "Mortgage used" check_answer_label: "Mortgage used"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Was a mortgage used for the purchase of this property?" question_text: "Was a mortgage used for the purchase of this property?"
mortgage: mortgage:
page_header: "Mortgage Amount" page_header: ""
check_answer_label: "Mortgage amount" check_answer_label: "Mortgage amount"
check_answer_prompt: "" 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." 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: age1_known:
check_answer_label: "" check_answer_label: ""
check_answer_prompt: "Answer if you know the lead tenant's age" 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?" question_text: "Do you know the lead tenant’s age?"
age1: age1:
check_answer_label: "Lead tenant’s age" 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_label: "Person 2’s gender identity"
check_answer_prompt: "" 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." 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: ethnic_group2:
page_header: "" page_header: ""
@ -209,7 +209,7 @@ en:
check_answer_label: "Person 2’s working situation" check_answer_label: "Person 2’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: buy2livein:
page_header: "" page_header: ""
@ -271,14 +271,14 @@ en:
check_answer_label: "Person 3’s gender identity" check_answer_label: "Person 3’s gender identity"
check_answer_prompt: "" 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." 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: ecstat3:
page_header: "" page_header: ""
check_answer_label: "Person 3’s working situation" check_answer_label: "Person 3’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_4:
page_header: "" page_header: ""
@ -312,14 +312,14 @@ en:
check_answer_label: "Person 4’s gender identity" check_answer_label: "Person 4’s gender identity"
check_answer_prompt: "" 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." 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: ecstat4:
page_header: "" page_header: ""
check_answer_label: "Person 4’s working situation" check_answer_label: "Person 4’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_5:
page_header: "" page_header: ""
@ -353,14 +353,14 @@ en:
check_answer_label: "Person 5’s gender identity" check_answer_label: "Person 5’s gender identity"
check_answer_prompt: "" 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." 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: ecstat5:
page_header: "" page_header: ""
check_answer_label: "Person 5’s working situation" check_answer_label: "Person 5’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: details_known_6:
page_header: "" page_header: ""
@ -394,11 +394,11 @@ en:
check_answer_label: "Person 6’s gender identity" check_answer_label: "Person 6’s gender identity"
check_answer_prompt: "" 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." 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: ecstat6:
page_header: "" page_header: ""
check_answer_label: "Person 6’s working situation" check_answer_label: "Person 6’s working situation"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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_label: "Buyers’ total savings known?"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" 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: savings:
check_answer_label: "Buyers’ total savings before any deposit paid" check_answer_label: "Buyers’ total savings before any deposit paid"
check_answer_prompt: "" 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?" question_text: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
frombeds: frombeds:
page_header: "About the buyers’ previous property" page_header: ""
check_answer_label: "Number of bedrooms in previous property" check_answer_label: "Number of bedrooms in previous property"
check_answer_prompt: "" 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?" question_text: "How many bedrooms did the property have?"
fromprop: fromprop:
@ -170,14 +170,14 @@ en:
question_text: "What was the percentage shared purchased in the initial transaction?" question_text: "What was the percentage shared purchased in the initial transaction?"
mortgageused: mortgageused:
page_header: "Mortgage Amount" page_header: ""
check_answer_label: "Mortgage used" check_answer_label: "Mortgage used"
check_answer_prompt: "" check_answer_prompt: "Answer if a mortgage was used"
hint_text: "" hint_text: ""
question_text: "Was a mortgage used for the purchase of this property?" question_text: "Was a mortgage used for the purchase of this property?"
mortgage: mortgage:
page_header: "Mortgage Amount" page_header: ""
check_answer_label: "Mortgage amount" check_answer_label: "Mortgage amount"
check_answer_prompt: "" 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." 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: "" page_header: ""
check_answer_label: "Staircasing transaction" check_answer_label: "Staircasing transaction"
check_answer_prompt: "" 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?" question_text: "Is this a staircasing transaction?"
type: 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" invalid: "Age of person %{person_num} must be a number or the letter R"
address: 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_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: nationality:
invalid: "Select a valid nationality." invalid: "Select a valid nationality."
charges: 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." 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: 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_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: nationality:
invalid: "Select a valid 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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.integer "increment_number", default: 1, null: false
t.boolean "empty_export", default: false, null: false t.boolean "empty_export", default: false, null: false
t.string "collection" t.string "collection"
t.integer "year"
end end
create_table "la_rent_ranges", force: :cascade do |t| 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 "discarded_at"
t.datetime "schemes_deduplicated_at" t.datetime "schemes_deduplicated_at"
t.index ["absorbing_organisation_id"], name: "index_organisations_on_absorbing_organisation_id" 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 t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true
end end

6
lib/tasks/data_export.rake

@ -7,13 +7,13 @@ namespace :core do
end end
desc "Export all data XMLs for import into Central Data System (CDS)" 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 = 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"]) storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["EXPORT_BUCKET"])
export_service = Exports::ExportService.new(storage_service) 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
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(:count) { 2 }
let(:item_label) { "user" } let(:item_label) { "user" }
let(:total_count) { 3 } let(:total_count) { 3 }
let(:item) { "schemes" } let(:item) { "scheme" }
let(:filters_count) { 1 } let(:filters_count) { 1 }
let(:result) { render_inline(described_class.new(searched:, count:, item_label:, total_count:, item:, filters_count:)) } 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 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") expect(result.to_html).to eq("<span>\n <strong>2</strong> users matching search<br>\n</span>\n")
end 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 end
context "when filter results are found" do 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 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") expect(result.to_html).to eq("<span>\n <strong>2</strong> users matching filters<br>\n</span>\n")
end 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 end
context "when no search/filter is applied" do 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 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") 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 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 end
context "when nothing is found" do 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) allow(Rails.env).to receive(:review?).and_return(true)
end end
it "sets up correct data" do # Doing this in one test should save ~2 minutes
it "sets up correct data idempotently" do
expect { expect {
Rails.application.load_seed Rails.application.load_seed
}.to change(User, :count) }.to change(User, :count)
@ -30,10 +31,6 @@ RSpec.describe "seeding process", type: task do
.and change(Scheme, :count) .and change(Scheme, :count)
.and change(Location, :count) .and change(Location, :count)
.and change(LaRentRange, :count) .and change(LaRentRange, :count)
end
it "is idempotent" do
Rails.application.load_seed
expect { expect {
Rails.application.load_seed 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 expect(duplicate_log.duplicate_set_id).to be_nil
end end
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
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") expect(page).to have_current_path("/sales-logs/bulk-uploads")
end end
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 end
context "when a log becomes a duplicate" do context "when a log becomes a duplicate" do

4
spec/fixtures/exports/general_needs_log.xml vendored

@ -147,10 +147,10 @@
<duplicate_set_id/> <duplicate_set_id/>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname> <owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum> <hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname> <maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum> <manhcnum>1234</manhcnum>
<createddate>2022-05-01T00:00:00+01:00</createddate> <createddate>2022-05-01T00:00:00+01:00</createddate>
<uploaddate>2022-05-01T00:00:00+01:00</uploaddate> <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/> <duplicate_set_id/>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname> <owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum> <hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname> <maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum> <manhcnum>1234</manhcnum>
<createddate>2023-04-03T00:00:00+01:00</createddate> <createddate>2023-04-03T00:00:00+01:00</createddate>
<uploaddate>2023-04-03T00:00:00+01:00</uploaddate> <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> <la_as_entered>la as entered</la_as_entered>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname> <owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum> <hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname> <maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum> <manhcnum>1234</manhcnum>
<createddate>2024-04-03T00:00:00+01:00</createddate> <createddate>2024-04-03T00:00:00+01:00</createddate>
<uploaddate>2024-04-03T00:00:00+01:00</uploaddate> <uploaddate>2024-04-03T00:00:00+01:00</uploaddate>

2
spec/fixtures/exports/organisation.xml vendored

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

4
spec/fixtures/exports/supported_housing_logs.xml vendored

@ -146,10 +146,10 @@
<duplicate_set_id/> <duplicate_set_id/>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname> <owningorgname>{owning_org_name}</owningorgname>
<hcnum>1234</hcnum> <hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname> <maningorgname>{managing_org_name}</maningorgname>
<manhcnum>1234</manhcnum> <manhcnum>1234</manhcnum>
<createddate>2022-05-01T00:00:00+01:00</createddate> <createddate>2022-05-01T00:00:00+01:00</createddate>
<uploaddate>2022-05-01T00:00:00+01:00</uploaddate> <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_dpo>false</is_dpo>
<is_key_contact>false</is_key_contact> <is_key_contact>false</is_key_contact>
<active>true</active> <active>true</active>
<organisation_name>MHCLG</organisation_name> <organisation_name>{organisation_name}</organisation_name>
</form> </form>
</forms> </forms>

2
spec/helpers/tab_nav_helper_spec.rb

@ -9,7 +9,7 @@ RSpec.describe TabNavHelper do
describe "#user_cell" do describe "#user_cell" do
it "returns user link and email separated by a newline character" 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>" 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
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 context "with all available years" do
it "calls the export service" 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 task.invoke
end end
@ -38,9 +38,9 @@ describe "rake core:data_export", type: task do
context "with a specific collection" do context "with a specific collection" do
it "calls the export service" 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 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:) mailer.send_check_soft_validations_mail(bulk_upload:)
end end
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 end

11
spec/mailers/devise_notify_mailer_spec.rb

@ -36,8 +36,11 @@ RSpec.describe DeviseNotifyMailer do
end end
context "when the email domain is in the allowlist" do context "when the email domain is in the allowlist" do
let(:domain) { Rails.application.credentials[:email_allowlist].first } before do
let(:email) { "test@#{domain}" } allow(Rails.application.credentials).to receive(:[]).with(:email_allowlist).and_return(["example.com"])
end
let(:email) { "test@example.com" }
it "does send emails" do it "does send emails" do
expect(notify_client).to receive(:send_email).once expect(notify_client).to receive(:send_email).once
@ -48,10 +51,10 @@ RSpec.describe DeviseNotifyMailer do
context "when notify mailer raises BadRequestError" do context "when notify mailer raises BadRequestError" do
before do before do
allow(notify_client).to receive(:send_email).and_raise(bad_request_error) 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 end
let(:domain) { Rails.application.credentials[:email_allowlist].first } let(:email) { "test@example.com" }
let(:email) { "test@#{domain}" }
it "does not raise an error" do it "does not raise an error" do
expect { 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 context "when organisation has merged" do
let(:absorbing_org) { create(:organisation, name: "Absorbing org", holds_own_stock: true) } 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_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(:user) { create(:user, :data_coordinator, organisation: absorbing_org) }
let(:log) do 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(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1))) } 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 it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
end 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(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1))) } 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 it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
end 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" }, "0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" }, "10" => { "value" => "Buyer prefers not to say" },
"7" => { "value" => "Full-time student" }, "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 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) } 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 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
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) } 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 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
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(: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)))) } 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 it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
end 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')}") .to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Provider type #{I18n.t('validations.organisation.provider_type_missing')}")
end 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 context "with parent/child associations", :aggregate_failures do
let!(:child_organisation) { create(:organisation, name: "MHCLG Child") } let!(:child_organisation) { create(:organisation, name: "MHCLG Child") }
let!(:grandchild_organisation) { create(:organisation, name: "MHCLG Grandchild") } 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 scheme.startdate = Time.zone.today + 2.weeks
expect(scheme.status).to eq(:activating_soon) expect(scheme.status).to eq(:activating_soon)
end 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 end
context "when there have been previous deactivations" do 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 context "when the user is in staging environment" do
before do before do
allow(Rails.env).to receive(:staging?).and_return(true) 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 end
context "and the user is not in the staging role update email allowlist" do 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") expect(response.body).to include("How to upload logs in bulk")
end end
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
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") expect(response.body).to include("How to upload logs in bulk")
end end
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
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 it "has search results in the title" do
get "/lettings-logs?search=#{log_to_search.id}", headers:, params: {} 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 end
it "shows lettings logs matching the id" do it "shows lettings logs matching the id" do
@ -895,7 +895,7 @@ RSpec.describe LettingsLogsController, type: :request do
end end
it "shows the total log count" do 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 end
it "does not show the pagination links" do it "does not show the pagination links" do
@ -1483,7 +1483,7 @@ RSpec.describe LettingsLogsController, type: :request do
end end
context "when viewing a collection of logs affected by deactivated location" do 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!(: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!(:non_affected_lettings_logs) { FactoryBot.create_list(:lettings_log, 4, assigned_to: user) }
let(:other_user) { FactoryBot.create(:user, organisation: user.organisation) } 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 it "shows a table of stock owners" do
expected_html = "<table class=\"govuk-table\"" expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html) 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 end
it "shows only stock owners for the current user's organisation" do it "shows only stock owners for the current user's organisation" do
@ -53,12 +53,12 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end end
it "shows the pagination count" do 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
context "when adding a stock owner" do context "when adding a stock owner" do
let!(:active_organisation) { FactoryBot.create(:organisation, name: "Active Org", active: true) } 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 before do
get "/organisations/#{organisation.id}/stock-owners/add", headers:, params: {} 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!(:managing_agent) { FactoryBot.create(:organisation) }
let!(:other_org_managing_agent) { FactoryBot.create(:organisation, name: "Foobar LTD") } let!(:other_org_managing_agent) { FactoryBot.create(:organisation, name: "Foobar LTD") }
let!(:inactive_managing_agent) { FactoryBot.create(:organisation, name: "Inactive LTD", active: false) } 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 before do
FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent) 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 it "shows a table of managing-agents" do
expected_html = "<table class=\"govuk-table\"" expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html) 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 end
it "shows only managing-agents for the current user's organisation" do it "shows only managing-agents for the current user's organisation" do
@ -149,7 +149,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end end
it "shows the pagination count" do 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
context "and current organisation is deactivated" do 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 context "with an organisation that the user belongs to" do
let!(:stock_owner) { FactoryBot.create(:organisation) } let!(:stock_owner) { FactoryBot.create(:organisation) }
let!(:other_org_stock_owner) { FactoryBot.create(:organisation, name: "Foobar LTD") } 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 before do
FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: stock_owner) 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 it "shows a table of stock owners" do
expected_html = "<table class=\"govuk-table\"" expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html) 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 end
it "shows only stock owners for the current user's organisation" do it "shows only stock owners for the current user's organisation" do
@ -345,7 +345,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end end
it "shows the pagination count" do 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
end end
@ -452,7 +452,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
context "with an organisation that the user belongs to" do context "with an organisation that the user belongs to" do
let!(:managing_agent) { FactoryBot.create(:organisation) } let!(:managing_agent) { FactoryBot.create(:organisation) }
let!(:other_org_managing_agent) { FactoryBot.create(:organisation, name: "Foobar LTD") } 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 before do
FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent) 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 it "shows a table of managing agents" do
expected_html = "<table class=\"govuk-table\"" expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html) 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 end
it "shows only managing agents for the current user's organisation" do it "shows only managing agents for the current user's organisation" do
@ -481,7 +481,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end end
it "shows the pagination count" do 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
end end
@ -647,7 +647,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end end
it "shows the pagination count" do 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
context "when adding a stock owner" do context "when adding a stock owner" do
@ -697,7 +697,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do
end end
it "shows the pagination count" do 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
it "shows remove link(s)" do 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