Browse Source

Merge branch 'main' into CLDC-3789-update-frontend-components

pull/2854/head
kosiakkatrina 1 year ago committed by GitHub
parent
commit
5bc619d5d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 411
      .github/workflows/run_tests.yml
  2. 347
      .github/workflows/staging_pipeline.yml
  3. 2
      Gemfile
  4. 124
      Gemfile.lock
  5. 2
      app/components/search_result_caption_component.html.erb
  6. 23
      app/controllers/bulk_upload_lettings_logs_controller.rb
  7. 19
      app/controllers/bulk_upload_sales_logs_controller.rb
  8. 11
      app/controllers/form_controller.rb
  9. 1
      app/controllers/organisations_controller.rb
  10. 5
      app/helpers/form_page_error_helper.rb
  11. 21
      app/mailers/bulk_upload_mailer.rb
  12. 2
      app/models/form/sales/pages/owning_organisation.rb
  13. 6
      app/models/form/sales/questions/buyer2_working_situation.rb
  14. 39
      app/models/forms/bulk_upload_lettings/needstype.rb
  15. 1
      app/models/organisation.rb
  16. 2
      app/models/scheme.rb
  17. 2
      app/models/validations/property_validations.rb
  18. 2
      app/models/validations/sales/property_validations.rb
  19. 14
      app/services/bulk_upload/lettings/validator.rb
  20. 40
      app/services/bulk_upload/processor.rb
  21. 12
      app/services/bulk_upload/sales/validator.rb
  22. 23
      app/views/bulk_upload_lettings_logs/forms/needstype.erb
  23. 2
      app/views/bulk_upload_shared/uploads.html.erb
  24. 4
      app/views/form/page.html.erb
  25. 2
      app/views/locations/index.html.erb
  26. 2
      app/views/logs/_log_list.html.erb
  27. 2
      app/views/logs/index.html.erb
  28. 2
      app/views/logs/update_logs.html.erb
  29. 4
      app/views/organisation_relationships/managing_agents.html.erb
  30. 4
      app/views/organisation_relationships/stock_owners.html.erb
  31. 2
      app/views/organisations/_organisation_list.html.erb
  32. 2
      app/views/organisations/index.html.erb
  33. 2
      app/views/organisations/logs.html.erb
  34. 2
      app/views/schemes/_scheme_list.html.erb
  35. 2
      app/views/users/_user_list.html.erb
  36. 1
      config/locales/en.yml
  37. 5
      db/migrate/20241125153349_add_unique_index_to_org_name.rb
  38. 1
      db/schema.rb
  39. 26
      spec/components/search_result_caption_component_spec.rb
  40. 7
      spec/db/seeds_spec.rb
  41. 2
      spec/factories/organisation.rb
  42. 16
      spec/features/form/page_routing_spec.rb
  43. 4
      spec/fixtures/exports/general_needs_log.xml
  44. 4
      spec/fixtures/exports/general_needs_log_23_24.xml
  45. 4
      spec/fixtures/exports/general_needs_log_24_25.xml
  46. 2
      spec/fixtures/exports/organisation.xml
  47. 4
      spec/fixtures/exports/supported_housing_logs.xml
  48. 2
      spec/fixtures/exports/user.xml
  49. 25
      spec/mailers/bulk_upload_mailer_spec.rb
  50. 11
      spec/mailers/devise_notify_mailer_spec.rb
  51. 2
      spec/models/form/lettings/questions/managing_organisation_spec.rb
  52. 28
      spec/models/form/sales/questions/buyer2_working_situation_spec.rb
  53. 6
      spec/models/organisation_spec.rb
  54. 7
      spec/models/scheme_spec.rb
  55. 1
      spec/models/user_spec.rb
  56. 111
      spec/requests/bulk_upload_lettings_logs_controller_spec.rb
  57. 111
      spec/requests/bulk_upload_sales_logs_controller_spec.rb
  58. 2
      spec/requests/check_errors_controller_spec.rb
  59. 4
      spec/requests/lettings_logs_controller_spec.rb
  60. 20
      spec/requests/organisation_relationships_controller_spec.rb
  61. 18
      spec/requests/organisations_controller_spec.rb
  62. 4
      spec/requests/sales_logs_controller_spec.rb
  63. 10
      spec/services/bulk_upload/lettings/validator_spec.rb
  64. 32
      spec/services/bulk_upload/processor_spec.rb
  65. 16
      spec/services/bulk_upload/sales/validator_spec.rb
  66. 2
      spec/services/exports/lettings_log_export_service_spec.rb
  67. 1
      spec/services/exports/organisation_export_service_spec.rb
  68. 1
      spec/services/exports/user_export_service_spec.rb
  69. 6
      yarn.lock

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

347
.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@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)']
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']
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
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

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

124
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)
@ -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.8-arm64-darwin) nokogiri (1.17.1-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.8-x86_64-darwin) nokogiri (1.17.1-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.8-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,20 +327,20 @@ 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
@ -354,9 +354,9 @@ GEM
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

11
app/controllers/form_controller.rb

@ -105,8 +105,13 @@ private
def restore_error_field_values(previous_responses) def restore_error_field_values(previous_responses)
return unless previous_responses return unless previous_responses
previous_responses_to_reset = previous_responses.reject do |key, _| previous_responses_to_reset = previous_responses.reject do |key, value|
@log.form.get_question(key, @log)&.type == "date" if @log.form.get_question(key, @log)&.type == "date" && value.present?
year = value.split("-").first.to_i
year&.zero?
else
false
end
end end
@log.assign_attributes(previous_responses_to_reset) @log.assign_attributes(previous_responses_to_reset)
@ -433,7 +438,7 @@ private
@log.valid? @log.valid?
@log.reload @log.reload
error_attributes = @log.errors.map(&:attribute) error_attributes = @log.errors.map(&:attribute)
@questions = @log.form.questions.select { |q| error_attributes.include?(q.id.to_sym) } @questions = @log.form.questions.select { |q| error_attributes.include?(q.id.to_sym) && q.page.routed_to?(@log, current_user) }
end end
render "form/check_errors" render "form/check_errors"
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

5
app/helpers/form_page_error_helper.rb

@ -13,7 +13,8 @@ module FormPageErrorHelper
end end
end end
def all_questions_affected_by_errors(log) def all_pages_affected_by_errors(log)
(log.errors.map(&:attribute) - [:base]).uniq question_ids = (log.errors.map(&:attribute) - [:base]).uniq
question_ids.map { |id| log.form.get_question(id, log)&.page&.id }.compact.uniq
end end
end end

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)

2
app/models/form/sales/pages/owning_organisation.rb

@ -20,11 +20,13 @@ class Form::Sales::Pages::OwningOrganisation < ::Form::Page
if current_user.organisation.holds_own_stock? if current_user.organisation.holds_own_stock?
return true if current_user.organisation.absorbed_organisations.any?(&:holds_own_stock?) return true if current_user.organisation.absorbed_organisations.any?(&:holds_own_stock?)
return true if stock_owners.count >= 1 return true if stock_owners.count >= 1
return false if log.owning_organisation == current_user.organisation
log.update!(owning_organisation: current_user.organisation) log.update!(owning_organisation: current_user.organisation)
else else
return false if stock_owners.count.zero? return false if stock_owners.count.zero?
return true if stock_owners.count > 1 return true if stock_owners.count > 1
return false if log.owning_organisation == stock_owners.first
log.update!(owning_organisation: stock_owners.first) log.update!(owning_organisation: stock_owners.first)
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

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

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

2
app/models/validations/property_validations.rb

@ -41,6 +41,8 @@ module Validations::PropertyValidations
def validate_property_postcode(record) def validate_property_postcode(record)
postcode = record.postcode_full postcode = record.postcode_full
return unless postcode
if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP)) if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP))
error_message = I18n.t("validations.lettings.property.postcode_full.invalid") error_message = I18n.t("validations.lettings.property.postcode_full.invalid")
record.errors.add :postcode_full, :wrong_format, message: error_message record.errors.add :postcode_full, :wrong_format, message: error_message

2
app/models/validations/sales/property_validations.rb

@ -31,6 +31,8 @@ module Validations::Sales::PropertyValidations
def validate_property_postcode(record) def validate_property_postcode(record)
postcode = record.postcode_full postcode = record.postcode_full
return unless postcode
if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP)) if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP))
error_message = I18n.t("validations.sales.property_information.postcode_full.invalid") error_message = I18n.t("validations.sales.property_information.postcode_full.invalid")
record.errors.add :postcode_full, :wrong_format, message: error_message record.errors.add :postcode_full, :wrong_format, message: error_message

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)

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?

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

4
app/views/form/page.html.erb

@ -16,7 +16,7 @@
<%= form_with model: @log, url: request.original_url, method: "post", local: true do |f| %> <%= form_with model: @log, url: request.original_url, method: "post", local: true do |f| %>
<div class="govuk-grid-row"> <div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop"> <div class="govuk-grid-column-two-thirds-from-desktop">
<% all_questions_with_errors = all_questions_affected_by_errors(@log) %> <% all_pages_with_errors = all_pages_affected_by_errors(@log) %>
<% remove_other_page_errors(@log, @page) %> <% remove_other_page_errors(@log, @page) %>
<%= f.govuk_error_summary %> <%= f.govuk_error_summary %>
@ -76,7 +76,7 @@
<%= f.hidden_field :check_errors, value: @check_errors %> <%= f.hidden_field :check_errors, value: @check_errors %>
<% end %> <% end %>
<% if all_questions_with_errors.count > 1 %> <% if all_pages_with_errors.count > 1 %>
<div class="govuk-button-group"> <div class="govuk-button-group">
<%= f.submit "See all related answers", name: "check_errors", class: "govuk-body govuk-link submit-button-link" %> <%= f.submit "See all related answers", name: "check_errors", class: "govuk-body govuk-link submit-button-link" %>
</div> </div>

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/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."

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

1
db/schema.rb

@ -546,6 +546,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_12_04_100518) 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

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

2
spec/factories/organisation.rb

@ -1,6 +1,6 @@
FactoryBot.define do FactoryBot.define do
factory :organisation do factory :organisation do
name { Faker::Company.name } sequence(:name) { |n| "#{Faker::Company.name} #{n}" }
address_line1 { Faker::Address.street_address } address_line1 { Faker::Address.street_address }
address_line2 { Faker::Address.city } address_line2 { Faker::Address.city }
provider_type { "LA" } provider_type { "LA" }

16
spec/features/form/page_routing_spec.rb

@ -92,7 +92,7 @@ RSpec.describe "Form Page Routing" do
expect(find("#lettings-log-postcode-full-field-error").value).to eq("FAKE_POSTCODE") expect(find("#lettings-log-postcode-full-field-error").value).to eq("FAKE_POSTCODE")
end end
it "does not reset the displayed date" do it "does not reset the displayed date if it's an invalid date" do
lettings_log.update!(startdate: "2021/10/13") lettings_log.update!(startdate: "2021/10/13")
visit("/lettings-logs/#{id}/tenancy-start-date") visit("/lettings-logs/#{id}/tenancy-start-date")
fill_in("lettings_log[startdate(1i)]", with: "202") fill_in("lettings_log[startdate(1i)]", with: "202")
@ -106,6 +106,20 @@ RSpec.describe "Form Page Routing" do
expect(find_field("lettings_log[startdate(1i)]").value).to eq("2021") expect(find_field("lettings_log[startdate(1i)]").value).to eq("2021")
end end
it "displays the entered date if it's in a valid format" do
lettings_log.update!(startdate: "2021/10/13")
visit("/lettings-logs/#{id}/tenancy-start-date")
fill_in("lettings_log[startdate(1i)]", with: "202")
fill_in("lettings_log[startdate(2i)]", with: "12")
fill_in("lettings_log[startdate(3i)]", with: "1")
click_button("Save and continue")
expect(page).to have_current_path("/lettings-logs/#{id}/tenancy-start-date")
expect(find_field("lettings_log[startdate(3i)]").value).to eq("1")
expect(find_field("lettings_log[startdate(2i)]").value).to eq("12")
expect(find_field("lettings_log[startdate(1i)]").value).to eq("202")
end
it "does not reset the displayed date if it's empty" do it "does not reset the displayed date if it's empty" do
lettings_log.update!(startdate: nil) lettings_log.update!(startdate: nil)
visit("/lettings-logs/#{id}/tenancy-start-date") visit("/lettings-logs/#{id}/tenancy-start-date")

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>

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

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

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

2
spec/requests/check_errors_controller_spec.rb

@ -84,7 +84,7 @@ RSpec.describe CheckErrorsController, type: :request do
end end
it "displays correct clear and change links" do it "displays correct clear and change links" do
expect(page.all(:button, value: "Clear").count).to eq(2) expect(page.all(:button, value: "Clear").count).to eq(1)
expect(page).to have_link("Change", count: 1) expect(page).to have_link("Change", count: 1)
expect(page).to have_button("Clear all") expect(page).to have_button("Clear all")
end end

4
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

20
spec/requests/organisation_relationships_controller_spec.rb

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

18
spec/requests/organisations_controller_spec.rb

@ -1203,7 +1203,7 @@ RSpec.describe OrganisationsController, type: :request do
it "has search results in the title" do it "has search results in the title" do
get "/organisations/#{organisation.id}/lettings-logs?search=#{log_to_search.id}", headers: headers, params: {} get "/organisations/#{organisation.id}/lettings-logs?search=#{log_to_search.id}", headers: headers, params: {}
expect(page).to have_title("#{organisation.name} (1 logs matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") expect(page).to have_title("#{organisation.name} (1 log matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end end
it "has search term in the search box" do it "has search term in the search box" do
@ -1352,7 +1352,7 @@ RSpec.describe OrganisationsController, type: :request do
it "has search results in the title" do it "has search results in the title" do
get "/organisations/#{organisation.id}/sales-logs?search=#{log_to_search.id}", headers: headers, params: {} get "/organisations/#{organisation.id}/sales-logs?search=#{log_to_search.id}", headers: headers, params: {}
expect(page).to have_title("#{organisation.name} (1 logs matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") expect(page).to have_title("#{organisation.name} (1 log matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end end
it "shows sales logs matching the id" do it "shows sales logs matching the id" do
@ -1555,7 +1555,10 @@ RSpec.describe OrganisationsController, type: :request do
let(:total_organisations_count) { Organisation.all.count } let(:total_organisations_count) { Organisation.all.count }
before do before do
create_list(:organisation, 25) build_list(:organisation, 25) do |organisation, index|
organisation.name = "Organisation #{index}"
organisation.save!
end
get "/organisations" get "/organisations"
end end
@ -1616,11 +1619,11 @@ RSpec.describe OrganisationsController, type: :request do
end end
it "updates the table caption" do it "updates the table caption" do
expect(page).to have_content("1 organisations matching search") expect(page).to have_content("1 organisation matching search")
end end
it "has search in the title" do it "has search in the title" do
expect(page).to have_title("Organisations (1 organisations matching ‘#{search_param}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") expect(page).to have_title("Organisations (1 organisation matching ‘#{search_param}’) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end end
context "when the search term matches more than 1 result" do context "when the search term matches more than 1 result" do
@ -1644,7 +1647,10 @@ RSpec.describe OrganisationsController, type: :request do
let(:search_param) { "MHCLG" } let(:search_param) { "MHCLG" }
before do before do
create_list(:organisation, 27, name: "MHCLG") build_list(:organisation, 27) do |organisation, index|
organisation.name = "MHCLG #{index}"
organisation.save!
end
get "/organisations?search=#{search_param}" get "/organisations?search=#{search_param}"
end end

4
spec/requests/sales_logs_controller_spec.rb

@ -610,7 +610,7 @@ RSpec.describe SalesLogsController, type: :request do
it "has search results in the title" do it "has search results in the title" do
get "/sales-logs?search=#{log_to_search.id}", headers: headers, params: {} get "/sales-logs?search=#{log_to_search.id}", headers: headers, params: {}
expect(page).to have_title("Sales logs (1 logs matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") expect(page).to have_title("Sales logs (1 log matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end end
it "shows sales logs matching the id" do it "shows sales logs matching the id" do
@ -692,7 +692,7 @@ RSpec.describe SalesLogsController, 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

10
spec/services/bulk_upload/lettings/validator_spec.rb

@ -232,7 +232,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
end end
end end
describe "#create_logs?" do describe "#block_log_creation_reason" do
context "when a log has a clearable, non-setup error" do context "when a log has a clearable, non-setup error" do
let(:log_1) { build(:lettings_log, :completed, period: 2, assigned_to: user) } let(:log_1) { build(:lettings_log, :completed, period: 2, assigned_to: user) }
let(:log_2) { build(:lettings_log, :completed, period: 2, assigned_to: user, age1: 5) } let(:log_2) { build(:lettings_log, :completed, period: 2, assigned_to: user, age1: 5) }
@ -245,7 +245,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
it "returns false" do it "returns false" do
validator.call validator.call
expect(validator).to be_create_logs expect(validator.block_log_creation_reason).to be_nil
end end
end end
@ -261,7 +261,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
it "returns true" do it "returns true" do
validator.call validator.call
expect(validator).to be_create_logs expect(validator.block_log_creation_reason).to be_nil
end end
end end
@ -277,7 +277,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
it "will not create logs" do it "will not create logs" do
validator.call validator.call
expect(validator).not_to be_create_logs expect(validator.block_log_creation_reason).to eq("setup_errors")
end end
end end
@ -291,7 +291,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
it "returns false" do it "returns false" do
validator.call validator.call
expect(validator).not_to be_create_logs expect(validator.block_log_creation_reason).to eq("setup_errors")
end end
end end
end end

32
spec/services/bulk_upload/processor_spec.rb

@ -14,7 +14,7 @@ RSpec.describe BulkUpload::Processor do
call: nil, call: nil,
total_logs_count: 1, total_logs_count: 1,
any_setup_errors?: false, any_setup_errors?: false,
create_logs?: true, block_log_creation_reason: nil,
soft_validation_errors_only?: false, soft_validation_errors_only?: false,
) )
end end
@ -165,7 +165,7 @@ RSpec.describe BulkUpload::Processor do
let(:log) { build(:lettings_log, :setup_completed, assigned_to: user) } let(:log) { build(:lettings_log, :setup_completed, assigned_to: user) }
before do before do
allow(mock_validator).to receive(:create_logs?).and_return(true) allow(mock_validator).to receive(:block_log_creation_reason).and_return(nil)
allow(mock_validator).to receive(:soft_validation_errors_only?).and_return(false) allow(mock_validator).to receive(:soft_validation_errors_only?).and_return(false)
allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true)
end end
@ -198,7 +198,7 @@ RSpec.describe BulkUpload::Processor do
context "when a bulk upload has logs with only soft validations triggered" do context "when a bulk upload has logs with only soft validations triggered" do
before do before do
allow(mock_validator).to receive(:create_logs?).and_return(true) allow(mock_validator).to receive(:block_log_creation_reason).and_return(nil)
allow(mock_validator).to receive(:soft_validation_errors_only?).and_return(true) allow(mock_validator).to receive(:soft_validation_errors_only?).and_return(true)
allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true)
end end
@ -239,7 +239,7 @@ RSpec.describe BulkUpload::Processor do
call: nil, call: nil,
total_logs_count: 1, total_logs_count: 1,
any_setup_errors?: false, any_setup_errors?: false,
create_logs?: false, block_log_creation_reason: "row_parser_block_log_creation",
) )
end end
@ -254,6 +254,30 @@ RSpec.describe BulkUpload::Processor do
expect(mail_double).to have_received(:deliver_later) expect(mail_double).to have_received(:deliver_later)
end end
end end
context "when upload has duplicate logs blocking log creation" do
let(:mock_validator) do
instance_double(
BulkUpload::Lettings::Validator,
invalid?: false,
call: nil,
total_logs_count: 1,
any_setup_errors?: false,
block_log_creation_reason: "duplicate_logs",
)
end
it "sends correct_and_upload_again_mail" do
mail_double = instance_double("ActionMailer::MessageDelivery", deliver_later: nil)
allow(BulkUploadMailer).to receive(:send_correct_duplicates_and_upload_again_mail).and_return(mail_double)
processor.call
expect(BulkUploadMailer).to have_received(:send_correct_duplicates_and_upload_again_mail)
expect(mail_double).to have_received(:deliver_later)
end
end
end end
describe "#approve" do describe "#approve" do

16
spec/services/bulk_upload/sales/validator_spec.rb

@ -204,7 +204,7 @@ RSpec.describe BulkUpload::Sales::Validator do
end end
end end
describe "#create_logs?" do describe "#block_log_creation_reason" do
context "when all logs are valid" do context "when all logs are valid" do
let(:log_1) { build(:sales_log, :completed, assigned_to: user) } let(:log_1) { build(:sales_log, :completed, assigned_to: user) }
let(:log_2) { build(:sales_log, :completed, assigned_to: user) } let(:log_2) { build(:sales_log, :completed, assigned_to: user) }
@ -214,9 +214,9 @@ RSpec.describe BulkUpload::Sales::Validator do
file.write(BulkUpload::SalesLogToCsv.new(log: log_2).to_csv_row) file.write(BulkUpload::SalesLogToCsv.new(log: log_2).to_csv_row)
end end
it "returns truthy" do it "returns nil" do
validator.call validator.call
expect(validator).to be_create_logs expect(validator.block_log_creation_reason).to be_nil
end end
end end
@ -229,9 +229,9 @@ RSpec.describe BulkUpload::Sales::Validator do
file.write(BulkUpload::SalesLogToCsv.new(log: log_2).to_csv_row) file.write(BulkUpload::SalesLogToCsv.new(log: log_2).to_csv_row)
end end
it "returns truthy" do it "returns nil" do
validator.call validator.call
expect(validator).to be_create_logs expect(validator.block_log_creation_reason).to be_nil
end end
end end
@ -245,9 +245,9 @@ RSpec.describe BulkUpload::Sales::Validator do
file.close file.close
end end
it "returns false" do it "returns the reason" do
validator.call validator.call
expect(validator).not_to be_create_logs expect(validator.block_log_creation_reason).to eq("setup_errors")
end end
end end
@ -262,7 +262,7 @@ RSpec.describe BulkUpload::Sales::Validator do
it "will not create logs" do it "will not create logs" do
validator.call validator.call
expect(validator).not_to be_create_logs expect(validator.block_log_creation_reason).to eq("setup_errors")
end end
end end
end end

2
spec/services/exports/lettings_log_export_service_spec.rb

@ -21,7 +21,9 @@ RSpec.describe Exports::LettingsLogExportService do
def replace_entity_ids(lettings_log, export_template) def replace_entity_ids(lettings_log, export_template)
export_template.sub!(/\{id\}/, (lettings_log["id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s) export_template.sub!(/\{id\}/, (lettings_log["id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s)
export_template.sub!(/\{owning_org_id\}/, (lettings_log["owning_organisation_id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s) export_template.sub!(/\{owning_org_id\}/, (lettings_log["owning_organisation_id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s)
export_template.sub!(/\{owning_org_name\}/, lettings_log.owning_organisation.name)
export_template.sub!(/\{managing_org_id\}/, (lettings_log["managing_organisation_id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s) export_template.sub!(/\{managing_org_id\}/, (lettings_log["managing_organisation_id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s)
export_template.sub!(/\{managing_org_name\}/, lettings_log.managing_organisation.name)
export_template.sub!(/\{location_id\}/, (lettings_log["location_id"]).to_s) if lettings_log.needstype == 2 export_template.sub!(/\{location_id\}/, (lettings_log["location_id"]).to_s) if lettings_log.needstype == 2
export_template.sub!(/\{scheme_id\}/, (lettings_log["scheme_id"]).to_s) if lettings_log.needstype == 2 export_template.sub!(/\{scheme_id\}/, (lettings_log["scheme_id"]).to_s) if lettings_log.needstype == 2
export_template.sub!(/\{log_id\}/, lettings_log["id"].to_s) export_template.sub!(/\{log_id\}/, lettings_log["id"].to_s)

1
spec/services/exports/organisation_export_service_spec.rb

@ -16,6 +16,7 @@ RSpec.describe Exports::OrganisationExportService do
def replace_entity_ids(organisation, export_template) def replace_entity_ids(organisation, export_template)
export_template.sub!(/\{id\}/, organisation["id"].to_s) export_template.sub!(/\{id\}/, organisation["id"].to_s)
export_template.sub!(/\{name\}/, organisation["name"])
export_template.sub!(/\{dsa_signed_at\}/, organisation.data_protection_confirmation&.signed_at.to_s) export_template.sub!(/\{dsa_signed_at\}/, organisation.data_protection_confirmation&.signed_at.to_s)
export_template.sub!(/\{dpo_email\}/, organisation.data_protection_confirmation&.data_protection_officer_email) export_template.sub!(/\{dpo_email\}/, organisation.data_protection_confirmation&.data_protection_officer_email)
end end

1
spec/services/exports/user_export_service_spec.rb

@ -17,6 +17,7 @@ RSpec.describe Exports::UserExportService do
def replace_entity_ids(user, export_template) def replace_entity_ids(user, export_template)
export_template.sub!(/\{id\}/, user["id"].to_s) export_template.sub!(/\{id\}/, user["id"].to_s)
export_template.sub!(/\{organisation_id\}/, user["organisation_id"].to_s) export_template.sub!(/\{organisation_id\}/, user["organisation_id"].to_s)
export_template.sub!(/\{organisation_name\}/, user.organisation.name)
export_template.sub!(/\{email\}/, user["email"].to_s) export_template.sub!(/\{email\}/, user["email"].to_s)
end end

6
yarn.lock

@ -4335,9 +4335,9 @@ mute-stream@0.0.8:
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
nanoid@^3.3.7: nanoid@^3.3.7:
version "3.3.7" version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"

Loading…
Cancel
Save