diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 000000000..1965a4034 --- /dev/null +++ b/.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 diff --git a/.github/workflows/staging_pipeline.yml b/.github/workflows/staging_pipeline.yml index aff0fe5f0..a2e777db0 100644 --- a/.github/workflows/staging_pipeline.yml +++ b/.github/workflows/staging_pipeline.yml @@ -4,11 +4,6 @@ on: push: branches: - main - pull_request: - types: - - opened - - synchronize - merge_group: workflow_dispatch: defaults: @@ -21,347 +16,13 @@ env: repository: core jobs: - test: - name: Tests - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:13.5 - env: - POSTGRES_PASSWORD: password - POSTGRES_USER: postgres - POSTGRES_DB: data_collector - ports: - - 5432:5432 - # Needed because the Postgres container does not provide a health check - # tmpfs makes database faster by using RAM - options: >- - --mount type=tmpfs,destination=/var/lib/postgresql/data - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - env: - RAILS_ENV: test - GEMFILE_RUBY_VERSION: 3.1.1 - DB_HOST: localhost - DB_DATABASE: data_collector - DB_USERNAME: postgres - DB_PASSWORD: password - RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - PARALLEL_TEST_PROCESSORS: 4 - - steps: - - name: Checkout - uses: actions/checkout@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 + tests: + name: Run Tests + uses: ./.github/workflows/run_tests.yml aws_deploy: name: AWS Deploy - if: github.ref == 'refs/heads/main' - needs: [lint, test, feature_test, requests_test, model_test, audit] + needs: [tests] uses: ./.github/workflows/aws_deploy.yml with: aws_account_id: 107155005276 diff --git a/Gemfile b/Gemfile index b9ba79e16..1028f921a 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.1.4" # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' -gem "rails", "~> 7.0.8.5" +gem "rails", "~> 7.0.8.7" # Use postgresql as the database for Active Record gem "pg", "~> 1.1" # Use Puma as the app server diff --git a/Gemfile.lock b/Gemfile.lock index 055257574..66e4c9c41 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,71 +1,71 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.5) - actionpack (= 7.0.8.5) - activesupport (= 7.0.8.5) + actioncable (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.5) - actionpack (= 7.0.8.5) - activejob (= 7.0.8.5) - activerecord (= 7.0.8.5) - activestorage (= 7.0.8.5) - activesupport (= 7.0.8.5) + actionmailbox (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.5) - actionpack (= 7.0.8.5) - actionview (= 7.0.8.5) - activejob (= 7.0.8.5) - activesupport (= 7.0.8.5) + actionmailer (7.0.8.7) + actionpack (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.5) - actionview (= 7.0.8.5) - activesupport (= 7.0.8.5) + actionpack (7.0.8.7) + actionview (= 7.0.8.7) + activesupport (= 7.0.8.7) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.5) - actionpack (= 7.0.8.5) - activerecord (= 7.0.8.5) - activestorage (= 7.0.8.5) - activesupport (= 7.0.8.5) + actiontext (7.0.8.7) + actionpack (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.5) - activesupport (= 7.0.8.5) + actionview (7.0.8.7) + activesupport (= 7.0.8.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.5) - activesupport (= 7.0.8.5) + activejob (7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.3.6) - activemodel (7.0.8.5) - activesupport (= 7.0.8.5) + activemodel (7.0.8.7) + activesupport (= 7.0.8.7) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (7.0.8.5) - activemodel (= 7.0.8.5) - activesupport (= 7.0.8.5) - activestorage (7.0.8.5) - actionpack (= 7.0.8.5) - activejob (= 7.0.8.5) - activerecord (= 7.0.8.5) - activesupport (= 7.0.8.5) + activerecord (7.0.8.7) + activemodel (= 7.0.8.7) + activesupport (= 7.0.8.7) + activestorage (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activesupport (= 7.0.8.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.5) + activesupport (7.0.8.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -149,7 +149,7 @@ GEM crass (1.0.6) cssbundling-rails (1.4.0) railties (>= 6.0.0) - date (3.3.4) + date (3.4.1) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) devise (4.9.3) @@ -258,13 +258,13 @@ GEM matrix (0.4.2) method_source (1.1.0) mini_mime (1.1.5) - minitest (5.25.1) + minitest (5.25.4) msgpack (1.7.2) multipart-post (2.4.1) nested_form (0.3.2) net-http (0.4.1) uri - net-imap (0.4.17) + net-imap (0.5.1) date net-protocol net-pop (0.1.2) @@ -273,12 +273,12 @@ GEM timeout net-smtp (0.5.0) net-protocol - nio4r (2.7.3) - nokogiri (1.16.8-arm64-darwin) + nio4r (2.7.4) + nokogiri (1.17.1-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.8-x86_64-darwin) + nokogiri (1.17.1-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.8-x86_64-linux) + nokogiri (1.17.1-x86_64-linux) racc (~> 1.4) notifications-ruby-client (6.0.0) jwt (>= 1.5, < 3) @@ -327,20 +327,20 @@ GEM rack (>= 1.2.0) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.5) - actioncable (= 7.0.8.5) - actionmailbox (= 7.0.8.5) - actionmailer (= 7.0.8.5) - actionpack (= 7.0.8.5) - actiontext (= 7.0.8.5) - actionview (= 7.0.8.5) - activejob (= 7.0.8.5) - activemodel (= 7.0.8.5) - activerecord (= 7.0.8.5) - activestorage (= 7.0.8.5) - activesupport (= 7.0.8.5) + rails (7.0.8.7) + actioncable (= 7.0.8.7) + actionmailbox (= 7.0.8.7) + actionmailer (= 7.0.8.7) + actionpack (= 7.0.8.7) + actiontext (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activemodel (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) bundler (>= 1.15.0) - railties (= 7.0.8.5) + railties (= 7.0.8.7) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -354,9 +354,9 @@ GEM nested_form (~> 0.3) rails (>= 6.0, < 8) turbo-rails (~> 1.0) - railties (7.0.8.5) - actionpack (= 7.0.8.5) - activesupport (= 7.0.8.5) + railties (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) method_source rake (>= 12.2) thor (~> 1.0) @@ -465,7 +465,7 @@ GEM thor (1.3.2) thread_safe (0.3.6) timecop (0.9.8) - timeout (0.4.1) + timeout (0.4.2) turbo-rails (1.5.0) actionpack (>= 6.0.0) activejob (>= 6.0.0) @@ -553,7 +553,7 @@ DEPENDENCIES rack (>= 2.2.6.3) rack-attack rack-mini-profiler (~> 2.0) - rails (~> 7.0.8.5) + rails (~> 7.0.8.7) rails_admin (~> 3.1) redcarpet (~> 3.6) redis (~> 4.8) diff --git a/app/components/search_result_caption_component.html.erb b/app/components/search_result_caption_component.html.erb index b8a9382b7..b2a28a505 100644 --- a/app/components/search_result_caption_component.html.erb +++ b/app/components/search_result_caption_component.html.erb @@ -7,7 +7,7 @@ <%= count %> <%= item_label.pluralize(count) %> matching filters
<% else %> - <%= count %> total <%= item %> + <%= count %> total <%= item.pluralize(count) %> <% end %> diff --git a/app/controllers/bulk_upload_lettings_logs_controller.rb b/app/controllers/bulk_upload_lettings_logs_controller.rb index a8ad14f9e..39bc05f7e 100644 --- a/app/controllers/bulk_upload_lettings_logs_controller.rb +++ b/app/controllers/bulk_upload_lettings_logs_controller.rb @@ -1,6 +1,7 @@ class BulkUploadLettingsLogsController < ApplicationController before_action :authenticate_user! - before_action :validate_data_protection_agrement_signed! + before_action :validate_data_protection_agreement_signed! + before_action :validate_year!, except: %w[start] def start if have_choice_of_year? @@ -24,12 +25,26 @@ class BulkUploadLettingsLogsController < ApplicationController private - def validate_data_protection_agrement_signed! + def validate_data_protection_agreement_signed! return if @current_user.organisation.data_protection_confirmed? redirect_to lettings_logs_path end + def validate_year! + return if params[:id] == "year" + return if params[:id] == "guidance" && params.dig(:form, :year).nil? + + allowed_years = [current_year] + allowed_years.push(current_year - 1) if FormHandler.instance.lettings_in_crossover_period? + allowed_years.push(current_year + 1) if FeatureToggle.allow_future_form_use? + + provided_year = params.dig(:form, :year)&.to_i + return if allowed_years.include?(provided_year) + + render_not_found + end + def current_year FormHandler.instance.current_collection_start_year end @@ -48,8 +63,6 @@ private Forms::BulkUploadLettings::PrepareYourFile.new(form_params) when "guidance" Forms::BulkUploadLettings::Guidance.new(form_params.merge(referrer: params[:referrer])) - when "needstype" - Forms::BulkUploadLettings::Needstype.new(form_params) when "upload-your-file" Forms::BulkUploadLettings::UploadYourFile.new(form_params.merge(current_user:)) when "checking-file" @@ -60,6 +73,6 @@ private end def form_params - params.fetch(:form, {}).permit(:year, :needstype, :file, :organisation_id) + params.fetch(:form, {}).permit(:year, :file, :organisation_id) end end diff --git a/app/controllers/bulk_upload_sales_logs_controller.rb b/app/controllers/bulk_upload_sales_logs_controller.rb index 2fd312d10..cb04cea95 100644 --- a/app/controllers/bulk_upload_sales_logs_controller.rb +++ b/app/controllers/bulk_upload_sales_logs_controller.rb @@ -1,6 +1,7 @@ class BulkUploadSalesLogsController < ApplicationController before_action :authenticate_user! - before_action :validate_data_protection_agrement_signed! + before_action :validate_data_protection_agreement_signed! + before_action :validate_year!, except: %w[start] def start if have_choice_of_year? @@ -24,12 +25,26 @@ class BulkUploadSalesLogsController < ApplicationController private - def validate_data_protection_agrement_signed! + def validate_data_protection_agreement_signed! return if @current_user.organisation.data_protection_confirmed? redirect_to sales_logs_path end + def validate_year! + return if params[:id] == "year" + return if params[:id] == "guidance" && params.dig(:form, :year).nil? + + allowed_years = [current_year] + allowed_years.push(current_year - 1) if FormHandler.instance.sales_in_crossover_period? + allowed_years.push(current_year + 1) if FeatureToggle.allow_future_form_use? + + provided_year = params.dig(:form, :year)&.to_i + return if allowed_years.include?(provided_year) + + render_not_found + end + def current_year FormHandler.instance.current_collection_start_year end diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb index 7ce63e609..3e029371d 100644 --- a/app/controllers/form_controller.rb +++ b/app/controllers/form_controller.rb @@ -105,8 +105,13 @@ private def restore_error_field_values(previous_responses) return unless previous_responses - previous_responses_to_reset = previous_responses.reject do |key, _| - @log.form.get_question(key, @log)&.type == "date" + previous_responses_to_reset = previous_responses.reject do |key, value| + if @log.form.get_question(key, @log)&.type == "date" && value.present? + year = value.split("-").first.to_i + year&.zero? + else + false + end end @log.assign_attributes(previous_responses_to_reset) @@ -433,7 +438,7 @@ private @log.valid? @log.reload 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 render "form/check_errors" end diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 8ffe426d7..93b667a99 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -155,6 +155,7 @@ class OrganisationsController < ApplicationController end redirect_to details_organisation_path(@organisation) else + @used_rent_periods = @organisation.lettings_logs.pluck(:period).uniq.compact.map(&:to_s) @rent_periods = helpers.rent_periods_with_checked_attr(checked_periods: selected_rent_periods) render :edit, status: :unprocessable_entity end diff --git a/app/helpers/form_page_error_helper.rb b/app/helpers/form_page_error_helper.rb index 2c90bfd2a..bf4e1db08 100644 --- a/app/helpers/form_page_error_helper.rb +++ b/app/helpers/form_page_error_helper.rb @@ -13,7 +13,8 @@ module FormPageErrorHelper end end - def all_questions_affected_by_errors(log) - (log.errors.map(&:attribute) - [:base]).uniq + def all_pages_affected_by_errors(log) + question_ids = (log.errors.map(&:attribute) - [:base]).uniq + question_ids.map { |id| log.form.get_question(id, log)&.page&.id }.compact.uniq end end diff --git a/app/mailers/bulk_upload_mailer.rb b/app/mailers/bulk_upload_mailer.rb index e0115abb0..7c62f71a1 100644 --- a/app/mailers/bulk_upload_mailer.rb +++ b/app/mailers/bulk_upload_mailer.rb @@ -3,6 +3,7 @@ class BulkUploadMailer < NotifyMailer COMPLETE_TEMPLATE_ID = "83279578-c890-4168-838b-33c9cf0dc9f0".freeze FAILED_CSV_ERRORS_TEMPLATE_ID = "e27abcd4-5295-48c2-b127-e9ee4b781b75".freeze + FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID = "931d5bda-a08f-4de6-a455-38a63bff1956".freeze FAILED_FILE_SETUP_ERROR_TEMPLATE_ID = "24c9f4c7-96ad-470a-ba31-eb51b7cbafd9".freeze FAILED_SERVICE_ERROR_TEMPLATE_ID = "c3f6288c-7a74-4e77-99ee-6c4a0f6e125a".freeze HOW_TO_FIX_UPLOAD_TEMPLATE_ID = "21a07b26-f625-4846-9f4d-39e30937aa24".freeze @@ -95,6 +96,26 @@ class BulkUploadMailer < NotifyMailer ) end + def send_correct_duplicates_and_upload_again_mail(bulk_upload:) + summary_report_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors? + bulk_upload.sales? ? summary_bulk_upload_sales_result_url(bulk_upload) : summary_bulk_upload_lettings_result_url(bulk_upload) + else + bulk_upload.sales? ? bulk_upload_sales_result_url(bulk_upload) : bulk_upload_lettings_result_url(bulk_upload) + end + + send_email( + bulk_upload.user.email, + FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID, + { + filename: bulk_upload.filename, + upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time), + year_combo: bulk_upload.year_combo, + lettings_or_sales: bulk_upload.log_type, + summary_report_link:, + }, + ) + end + def send_bulk_upload_failed_file_setup_error_mail(bulk_upload:) bulk_upload_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors? bulk_upload.sales? ? summary_bulk_upload_sales_result_url(bulk_upload) : summary_bulk_upload_lettings_result_url(bulk_upload) diff --git a/app/models/form/sales/pages/owning_organisation.rb b/app/models/form/sales/pages/owning_organisation.rb index f0c9e4e68..f1875d52d 100644 --- a/app/models/form/sales/pages/owning_organisation.rb +++ b/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? return true if current_user.organisation.absorbed_organisations.any?(&:holds_own_stock?) return true if stock_owners.count >= 1 + return false if log.owning_organisation == current_user.organisation log.update!(owning_organisation: current_user.organisation) else return false if stock_owners.count.zero? return true if stock_owners.count > 1 + return false if log.owning_organisation == stock_owners.first log.update!(owning_organisation: stock_owners.first) end diff --git a/app/models/form/sales/questions/buyer2_working_situation.rb b/app/models/form/sales/questions/buyer2_working_situation.rb index 38ab320b3..9105b8597 100644 --- a/app/models/form/sales/questions/buyer2_working_situation.rb +++ b/app/models/form/sales/questions/buyer2_working_situation.rb @@ -15,6 +15,10 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question @question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max] end + def displayed_answer_options(_log, _user = nil) + answer_options.reject { |key, _| key == "9" } + end + def answer_options if form.start_year_2025_or_later? { @@ -26,6 +30,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question "6" => { "value" => "Not seeking work" }, "7" => { "value" => "Full-time student" }, "8" => { "value" => "Unable to work due to long term sick or disability" }, + "9" => { "value" => "Child under 16" }, "0" => { "value" => "Other" }, "10" => { "value" => "Buyer prefers not to say" }, }.freeze @@ -41,6 +46,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question "0" => { "value" => "Other" }, "10" => { "value" => "Buyer prefers not to say" }, "7" => { "value" => "Full-time student" }, + "9" => { "value" => "Child under 16" }, }.freeze end end diff --git a/app/models/forms/bulk_upload_lettings/needstype.rb b/app/models/forms/bulk_upload_lettings/needstype.rb deleted file mode 100644 index b15c05b52..000000000 --- a/app/models/forms/bulk_upload_lettings/needstype.rb +++ /dev/null @@ -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 diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 23f91f1ad..69c80d198 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -58,6 +58,7 @@ class Organisation < ApplicationRecord alias_method :la?, :LA? validates :name, presence: { message: I18n.t("validations.organisation.name_missing") } + validates :name, uniqueness: { case_sensitive: false, message: I18n.t("validations.organisation.name_not_unique") } validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_missing") } def self.find_by_id_on_multiple_fields(id) diff --git a/app/models/scheme.rb b/app/models/scheme.rb index 1cd56ac7d..a9b479342 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -329,9 +329,9 @@ class Scheme < ApplicationRecord def status_at(date) return :deleted if discarded_at.present? - return :incomplete unless confirmed && locations.confirmed.any? return :deactivated if owning_organisation.status_at(date) == :deactivated || owning_organisation.status_at(date) == :merged || (open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date) + return :incomplete unless confirmed && locations.confirmed.any? return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date return :activating_soon if startdate.present? && date < startdate diff --git a/app/models/validations/property_validations.rb b/app/models/validations/property_validations.rb index e9eba8184..1cf710857 100644 --- a/app/models/validations/property_validations.rb +++ b/app/models/validations/property_validations.rb @@ -41,6 +41,8 @@ module Validations::PropertyValidations def validate_property_postcode(record) postcode = record.postcode_full + return unless postcode + if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP)) error_message = I18n.t("validations.lettings.property.postcode_full.invalid") record.errors.add :postcode_full, :wrong_format, message: error_message diff --git a/app/models/validations/sales/property_validations.rb b/app/models/validations/sales/property_validations.rb index 2238a634a..7fd4d2440 100644 --- a/app/models/validations/sales/property_validations.rb +++ b/app/models/validations/sales/property_validations.rb @@ -31,6 +31,8 @@ module Validations::Sales::PropertyValidations def validate_property_postcode(record) postcode = record.postcode_full + return unless postcode + if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP)) error_message = I18n.t("validations.sales.property_information.postcode_full.invalid") record.errors.add :postcode_full, :wrong_format, message: error_message diff --git a/app/services/bulk_upload/lettings/validator.rb b/app/services/bulk_upload/lettings/validator.rb index 116c3b745..291bf45e7 100644 --- a/app/services/bulk_upload/lettings/validator.rb +++ b/app/services/bulk_upload/lettings/validator.rb @@ -40,29 +40,25 @@ class BulkUpload::Lettings::Validator end end - def create_logs? - return false if any_setup_errors? + def block_log_creation_reason + return "setup_errors" if any_setup_errors? if row_parsers.any?(&:block_log_creation?) Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.") - return false + return "row_parser_block_log_creation" end if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled? - Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.") - return false + return "duplicate_logs" end row_parsers.each do |row_parser| row_parser.log.blank_invalid_non_setup_fields! end - if any_logs_invalid? Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.") - return false + "logs_invalid" end - - true end def self.question_for_field(field) diff --git a/app/services/bulk_upload/processor.rb b/app/services/bulk_upload/processor.rb index 38f67ede4..c54032fda 100644 --- a/app/services/bulk_upload/processor.rb +++ b/app/services/bulk_upload/processor.rb @@ -36,20 +36,28 @@ class BulkUpload::Processor if validator.any_setup_errors? send_setup_errors_mail - - elsif validator.create_logs? - create_logs - - if validator.soft_validation_errors_only? - send_check_soft_validations_mail - elsif created_logs_but_incompleted? - send_how_to_fix_upload_mail - elsif created_logs_and_all_completed? - bulk_upload.unpend - send_success_mail - end else - send_correct_and_upload_again_mail # summary/full report + block_creation_reason = validator.block_log_creation_reason + + if block_creation_reason.present? + case block_creation_reason + when "duplicate_logs" + send_correct_duplicates_and_upload_again_mail + else + send_correct_and_upload_again_mail # summary/full report + end + else + create_logs + + if validator.soft_validation_errors_only? + send_check_soft_validations_mail + elsif created_logs_but_incompleted? + send_how_to_fix_upload_mail + elsif created_logs_and_all_completed? + bulk_upload.unpend + send_success_mail + end + end end rescue StandardError => e Sentry.capture_exception(e) @@ -97,6 +105,12 @@ private .deliver_later end + def send_correct_duplicates_and_upload_again_mail + BulkUploadMailer + .send_correct_duplicates_and_upload_again_mail(bulk_upload:) + .deliver_later + end + def send_success_mail BulkUploadMailer .send_bulk_upload_complete_mail(user:, bulk_upload:) diff --git a/app/services/bulk_upload/sales/validator.rb b/app/services/bulk_upload/sales/validator.rb index 76fb6f1ae..7ad9638d7 100644 --- a/app/services/bulk_upload/sales/validator.rb +++ b/app/services/bulk_upload/sales/validator.rb @@ -39,17 +39,17 @@ class BulkUpload::Sales::Validator end end - def create_logs? - return false if any_setup_errors? + def block_log_creation_reason + return "setup_errors" if any_setup_errors? if row_parsers.any?(&:block_log_creation?) Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.") - return false + return "row_parser_block_log_creation" end if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled? Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.") - return false + return "duplicate_logs" end row_parsers.each do |row_parser| @@ -58,10 +58,8 @@ class BulkUpload::Sales::Validator if any_logs_invalid? Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.") - return false + "logs_invalid" end - - true end def any_setup_errors? diff --git a/app/views/bulk_upload_lettings_logs/forms/needstype.erb b/app/views/bulk_upload_lettings_logs/forms/needstype.erb deleted file mode 100644 index 644dd9f5f..000000000 --- a/app/views/bulk_upload_lettings_logs/forms/needstype.erb +++ /dev/null @@ -1,23 +0,0 @@ -<% content_for :before_content do %> - <%= govuk_back_link href: @form.back_path %> -<% end %> - -
-
- <%= 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 %> -
-
diff --git a/app/views/bulk_upload_shared/uploads.html.erb b/app/views/bulk_upload_shared/uploads.html.erb index 958887453..a9d134c60 100644 --- a/app/views/bulk_upload_shared/uploads.html.erb +++ b/app/views/bulk_upload_shared/uploads.html.erb @@ -1,4 +1,4 @@ -<% item_label = format_label(@pagy.count, "uploads") %> +<% item_label = format_label(@pagy.count, "upload") %> <% title = format_title(@searched, bulk_upload_title(controller.controller_name), current_user, item_label, @pagy.count, nil) %> <% content_for :title, title %> diff --git a/app/views/form/page.html.erb b/app/views/form/page.html.erb index 95dc38ec6..7379de0eb 100644 --- a/app/views/form/page.html.erb +++ b/app/views/form/page.html.erb @@ -16,7 +16,7 @@ <%= form_with model: @log, url: request.original_url, method: "post", local: true do |f| %>
- <% 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) %> <%= f.govuk_error_summary %> @@ -76,7 +76,7 @@ <%= f.hidden_field :check_errors, value: @check_errors %> <% end %> - <% if all_questions_with_errors.count > 1 %> + <% if all_pages_with_errors.count > 1 %>
<%= f.submit "See all related answers", name: "check_errors", class: "govuk-body govuk-link submit-button-link" %>
diff --git a/app/views/locations/index.html.erb b/app/views/locations/index.html.erb index 23550f894..8ef5bcb56 100644 --- a/app/views/locations/index.html.erb +++ b/app/views/locations/index.html.erb @@ -32,7 +32,7 @@ count: @pagy.count, item_label:, total_count: @total_count, - item: "locations", + item: "location", filters_count: applied_filters_count(@filter_type), )) %> <% end %> diff --git a/app/views/logs/_log_list.html.erb b/app/views/logs/_log_list.html.erb index b5290c117..24714f247 100644 --- a/app/views/logs/_log_list.html.erb +++ b/app/views/logs/_log_list.html.erb @@ -1,7 +1,7 @@

- <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "logs", filters_count: applied_filters_count(@filter_type))) %> + <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "log", filters_count: applied_filters_count(@filter_type))) %> <% if logs&.any? %> <%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> <% if @current_user.support? %> diff --git a/app/views/logs/index.html.erb b/app/views/logs/index.html.erb index f142a2580..c51466097 100644 --- a/app/views/logs/index.html.erb +++ b/app/views/logs/index.html.erb @@ -1,4 +1,4 @@ -<% item_label = format_label(@pagy.count, "logs") %> +<% item_label = format_label(@pagy.count, "log") %> <% title = format_title(@searched, "#{log_type_for_controller(controller).capitalize} logs", current_user, item_label, @pagy.count, nil) %> <% content_for :title, title %> diff --git a/app/views/logs/update_logs.html.erb b/app/views/logs/update_logs.html.erb index 1ab1fa31c..985adecf3 100644 --- a/app/views/logs/update_logs.html.erb +++ b/app/views/logs/update_logs.html.erb @@ -1,4 +1,4 @@ -<% item_label = format_label(@pagy.count, "logs") %> +<% item_label = format_label(@pagy.count, "log") %> <% title = "Update logs" %> <% content_for :title, title %> diff --git a/app/views/organisation_relationships/managing_agents.html.erb b/app/views/organisation_relationships/managing_agents.html.erb index 726533e53..d09056d86 100644 --- a/app/views/organisation_relationships/managing_agents.html.erb +++ b/app/views/organisation_relationships/managing_agents.html.erb @@ -1,4 +1,4 @@ -<% item_label = format_label(@pagy.count, "managing agents") %> +<% item_label = format_label(@pagy.count, "managing agent") %> <% if current_user.support? %> <%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %> @@ -44,7 +44,7 @@ pagy: @pagy, searched: @searched, item_label:, - search_item: "managing agents", + search_item: "managing agent", total_count: @total_count, remove_path: ->(org_id) { managing_agents_remove_organisation_path(target_organisation_id: org_id) }, } %> diff --git a/app/views/organisation_relationships/stock_owners.html.erb b/app/views/organisation_relationships/stock_owners.html.erb index 41b7af06d..3bbba6bf8 100644 --- a/app/views/organisation_relationships/stock_owners.html.erb +++ b/app/views/organisation_relationships/stock_owners.html.erb @@ -1,4 +1,4 @@ -<% item_label = format_label(@pagy.count, "stock owners") %> +<% item_label = format_label(@pagy.count, "stock owner") %> <% if current_user.support? %> <%= render partial: "organisations/headings", locals: { main: @organisation.name, sub: nil } %> <%= render SubNavigationComponent.new(items: secondary_items(request.path, @organisation.id)) %> @@ -41,7 +41,7 @@ pagy: @pagy, searched: @searched, item_label:, - search_item: "stock owners", + search_item: "stock owner", total_count: @total_count, remove_path: ->(org_id) { stock_owners_remove_organisation_path(target_organisation_id: org_id) }, } %> diff --git a/app/views/organisations/_organisation_list.html.erb b/app/views/organisations/_organisation_list.html.erb index 67cc9c7a3..16309a5eb 100644 --- a/app/views/organisations/_organisation_list.html.erb +++ b/app/views/organisations/_organisation_list.html.erb @@ -1,7 +1,7 @@
<%= govuk_table do |table| %> <%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> - <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "organisations", filters_count: applied_filters_count(@filter_type))) %> + <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "organisation", filters_count: applied_filters_count(@filter_type))) %> <% end %> <%= table.with_head do |head| %> <%= head.with_row do |row| %> diff --git a/app/views/organisations/index.html.erb b/app/views/organisations/index.html.erb index 411d792c1..1de12ab77 100644 --- a/app/views/organisations/index.html.erb +++ b/app/views/organisations/index.html.erb @@ -1,4 +1,4 @@ -<% item_label = format_label(@pagy.count, "organisations") %> +<% item_label = format_label(@pagy.count, "organisation") %> <% title = format_title(@searched, "Organisations", current_user, item_label, @pagy.count, nil) %> <% content_for :title, title %> diff --git a/app/views/organisations/logs.html.erb b/app/views/organisations/logs.html.erb index e172a76a9..e318ff6ee 100644 --- a/app/views/organisations/logs.html.erb +++ b/app/views/organisations/logs.html.erb @@ -1,4 +1,4 @@ -<% item_label = format_label(@pagy.count, "logs") %> +<% item_label = format_label(@pagy.count, "log") %> <% title = format_title(@searched, action_name.humanize, current_user, item_label, @pagy.count, @organisation.name) %> <% content_for :title, title %> diff --git a/app/views/schemes/_scheme_list.html.erb b/app/views/schemes/_scheme_list.html.erb index 967295236..1c11e86d1 100644 --- a/app/views/schemes/_scheme_list.html.erb +++ b/app/views/schemes/_scheme_list.html.erb @@ -2,7 +2,7 @@ <%= govuk_table do |table| %> <%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> - <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "schemes", filters_count: applied_filters_count(@filter_type))) %> + <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "scheme", filters_count: applied_filters_count(@filter_type))) %> <% if @schemes&.any? %> <%= govuk_link_to "Download schemes (CSV)", schemes_csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> <%= govuk_link_to "Download locations (CSV)", locations_csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> diff --git a/app/views/users/_user_list.html.erb b/app/views/users/_user_list.html.erb index 436c0def2..82a82b33b 100644 --- a/app/views/users/_user_list.html.erb +++ b/app/views/users/_user_list.html.erb @@ -1,7 +1,7 @@
<%= govuk_table do |table| %> <%= table.with_caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> - <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "users", filters_count: applied_filters_count(@filter_type))) %> + <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "user", filters_count: applied_filters_count(@filter_type))) %> <% if current_user.support? %> <% query = searched.present? ? "?search=#{searched}" : nil %> <%= govuk_link_to "Download (CSV)", "#{request.path}.csv#{query}", type: "text/csv", style: "white-space: nowrap" %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 851a9ea2c..965c1f7a6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -235,6 +235,7 @@ en: organisation: data_sharing_agreement_not_signed: "Your organisation must accept the Data Sharing Agreement before you can create any logs." name_missing: "Enter the name of the organisation." + name_not_unique: "An organisation with this name already exists. Use the organisation that already exists or add a location or other identifier to the name. For example: Organisation name (City)." provider_type_missing: "Select the organisation type." stock_owner: blank: "You must choose a stock owner." diff --git a/db/migrate/20241125153349_add_unique_index_to_org_name.rb b/db/migrate/20241125153349_add_unique_index_to_org_name.rb new file mode 100644 index 000000000..a7a124183 --- /dev/null +++ b/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 diff --git a/db/schema.rb b/db/schema.rb index d31a54da2..1a1c127c6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -546,6 +546,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_12_04_100518) do t.datetime "discarded_at" t.datetime "schemes_deduplicated_at" t.index ["absorbing_organisation_id"], name: "index_organisations_on_absorbing_organisation_id" + t.index ["name"], name: "index_organisations_on_name", unique: true t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true end diff --git a/spec/components/search_result_caption_component_spec.rb b/spec/components/search_result_caption_component_spec.rb index 25cbc1bdd..05ac09630 100644 --- a/spec/components/search_result_caption_component_spec.rb +++ b/spec/components/search_result_caption_component_spec.rb @@ -5,7 +5,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do let(:count) { 2 } let(:item_label) { "user" } let(:total_count) { 3 } - let(:item) { "schemes" } + let(:item) { "scheme" } let(:filters_count) { 1 } let(:result) { render_inline(described_class.new(searched:, count:, item_label:, total_count:, item:, filters_count:)) } @@ -21,6 +21,14 @@ RSpec.describe SearchResultCaptionComponent, type: :component do it "renders table caption including the search results and total" do expect(result.to_html).to eq("\n 2 users matching search
\n
\n") end + + context "with 1 result" do + let(:count) { 1 } + + it "renders table caption including the search results and total" do + expect(result.to_html).to eq("\n 1 user matching search
\n
\n") + end + end end context "when filter results are found" do @@ -29,6 +37,14 @@ RSpec.describe SearchResultCaptionComponent, type: :component do it "renders table caption including the search results and total" do expect(result.to_html).to eq("\n 2 users matching filters
\n
\n") end + + context "with 1 result" do + let(:count) { 1 } + + it "renders table caption including the search results and total" do + expect(result.to_html).to eq("\n 1 user matching filters
\n
\n") + end + end end context "when no search/filter is applied" do @@ -38,6 +54,14 @@ RSpec.describe SearchResultCaptionComponent, type: :component do it "renders table caption with total count only" do expect(result.to_html).to eq("\n \n 2 total schemes\n \n\n") end + + context "with 1 result" do + let(:count) { 1 } + + it "renders table caption with total count only" do + expect(result.to_html).to eq("\n \n 1 total scheme\n \n\n") + end + end end context "when nothing is found" do diff --git a/spec/db/seeds_spec.rb b/spec/db/seeds_spec.rb index 316f04ba6..6ae07ddb0 100644 --- a/spec/db/seeds_spec.rb +++ b/spec/db/seeds_spec.rb @@ -21,7 +21,8 @@ RSpec.describe "seeding process", type: task do allow(Rails.env).to receive(:review?).and_return(true) end - it "sets up correct data" do + # Doing this in one test should save ~2 minutes + it "sets up correct data idempotently" do expect { Rails.application.load_seed }.to change(User, :count) @@ -30,10 +31,6 @@ RSpec.describe "seeding process", type: task do .and change(Scheme, :count) .and change(Location, :count) .and change(LaRentRange, :count) - end - - it "is idempotent" do - Rails.application.load_seed expect { Rails.application.load_seed diff --git a/spec/factories/organisation.rb b/spec/factories/organisation.rb index ecab5bf81..f3184277e 100644 --- a/spec/factories/organisation.rb +++ b/spec/factories/organisation.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :organisation do - name { Faker::Company.name } + sequence(:name) { |n| "#{Faker::Company.name} #{n}" } address_line1 { Faker::Address.street_address } address_line2 { Faker::Address.city } provider_type { "LA" } diff --git a/spec/features/form/page_routing_spec.rb b/spec/features/form/page_routing_spec.rb index 42a2c25fb..118b52543 100644 --- a/spec/features/form/page_routing_spec.rb +++ b/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") 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") visit("/lettings-logs/#{id}/tenancy-start-date") 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") 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 lettings_log.update!(startdate: nil) visit("/lettings-logs/#{id}/tenancy-start-date") diff --git a/spec/fixtures/exports/general_needs_log.xml b/spec/fixtures/exports/general_needs_log.xml index bacc7e9f0..0341dd2d4 100644 --- a/spec/fixtures/exports/general_needs_log.xml +++ b/spec/fixtures/exports/general_needs_log.xml @@ -147,10 +147,10 @@ {id} {owning_org_id} - MHCLG + {owning_org_name} 1234 {managing_org_id} - MHCLG + {managing_org_name} 1234 2022-05-01T00:00:00+01:00 2022-05-01T00:00:00+01:00 diff --git a/spec/fixtures/exports/general_needs_log_23_24.xml b/spec/fixtures/exports/general_needs_log_23_24.xml index 9635cd0e4..ef0c4066c 100644 --- a/spec/fixtures/exports/general_needs_log_23_24.xml +++ b/spec/fixtures/exports/general_needs_log_23_24.xml @@ -148,10 +148,10 @@ {id} {owning_org_id} - MHCLG + {owning_org_name} 1234 {managing_org_id} - MHCLG + {managing_org_name} 1234 2023-04-03T00:00:00+01:00 2023-04-03T00:00:00+01:00 diff --git a/spec/fixtures/exports/general_needs_log_24_25.xml b/spec/fixtures/exports/general_needs_log_24_25.xml index a665a284e..00d8bb1a5 100644 --- a/spec/fixtures/exports/general_needs_log_24_25.xml +++ b/spec/fixtures/exports/general_needs_log_24_25.xml @@ -161,10 +161,10 @@ la as entered {id} {owning_org_id} - MHCLG + {owning_org_name} 1234 {managing_org_id} - MHCLG + {managing_org_name} 1234 2024-04-03T00:00:00+01:00 2024-04-03T00:00:00+01:00 diff --git a/spec/fixtures/exports/organisation.xml b/spec/fixtures/exports/organisation.xml index 8d87da16c..70c699915 100644 --- a/spec/fixtures/exports/organisation.xml +++ b/spec/fixtures/exports/organisation.xml @@ -2,7 +2,7 @@
{id} - MHCLG + {name} 1 2 Marsham Street diff --git a/spec/fixtures/exports/supported_housing_logs.xml b/spec/fixtures/exports/supported_housing_logs.xml index 50649241b..310d5ba2b 100644 --- a/spec/fixtures/exports/supported_housing_logs.xml +++ b/spec/fixtures/exports/supported_housing_logs.xml @@ -146,10 +146,10 @@ {id} {owning_org_id} - MHCLG + {owning_org_name} 1234 {managing_org_id} - MHCLG + {managing_org_name} 1234 2022-05-01T00:00:00+01:00 2022-05-01T00:00:00+01:00 diff --git a/spec/fixtures/exports/user.xml b/spec/fixtures/exports/user.xml index 5652ac9c6..d29a33225 100644 --- a/spec/fixtures/exports/user.xml +++ b/spec/fixtures/exports/user.xml @@ -12,6 +12,6 @@ false false true - MHCLG + {organisation_name}
diff --git a/spec/mailers/bulk_upload_mailer_spec.rb b/spec/mailers/bulk_upload_mailer_spec.rb index c3225c937..910bca4a9 100644 --- a/spec/mailers/bulk_upload_mailer_spec.rb +++ b/spec/mailers/bulk_upload_mailer_spec.rb @@ -121,4 +121,29 @@ RSpec.describe BulkUploadMailer do mailer.send_check_soft_validations_mail(bulk_upload:) end end + + describe "#send_correct_duplicates_and_upload_again_mail" do + context "when 2 columns with errors" do + before do + create(:bulk_upload_error, bulk_upload:, col: "A") + create(:bulk_upload_error, bulk_upload:, col: "B") + end + + it "sends correctly formed email" do + expect(notify_client).to receive(:send_email).with( + email_address: user.email, + template_id: described_class::FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID, + personalisation: { + filename: bulk_upload.filename, + upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time), + year_combo: bulk_upload.year_combo, + lettings_or_sales: bulk_upload.log_type, + summary_report_link: "http://localhost:3000/lettings-logs/bulk-upload-results/#{bulk_upload.id}", + }, + ) + + mailer.send_correct_duplicates_and_upload_again_mail(bulk_upload:) + end + end + end end diff --git a/spec/mailers/devise_notify_mailer_spec.rb b/spec/mailers/devise_notify_mailer_spec.rb index 4ed209b24..7a7123be0 100644 --- a/spec/mailers/devise_notify_mailer_spec.rb +++ b/spec/mailers/devise_notify_mailer_spec.rb @@ -36,8 +36,11 @@ RSpec.describe DeviseNotifyMailer do end context "when the email domain is in the allowlist" do - let(:domain) { Rails.application.credentials[:email_allowlist].first } - let(:email) { "test@#{domain}" } + before do + allow(Rails.application.credentials).to receive(:[]).with(:email_allowlist).and_return(["example.com"]) + end + + let(:email) { "test@example.com" } it "does send emails" do expect(notify_client).to receive(:send_email).once @@ -48,10 +51,10 @@ RSpec.describe DeviseNotifyMailer do context "when notify mailer raises BadRequestError" do before do allow(notify_client).to receive(:send_email).and_raise(bad_request_error) + allow(Rails.application.credentials).to receive(:[]).with(:email_allowlist).and_return(["example.com"]) end - let(:domain) { Rails.application.credentials[:email_allowlist].first } - let(:email) { "test@#{domain}" } + let(:email) { "test@example.com" } it "does not raise an error" do expect { diff --git a/spec/models/form/lettings/questions/managing_organisation_spec.rb b/spec/models/form/lettings/questions/managing_organisation_spec.rb index 776a873a6..00485e80f 100644 --- a/spec/models/form/lettings/questions/managing_organisation_spec.rb +++ b/spec/models/form/lettings/questions/managing_organisation_spec.rb @@ -185,7 +185,7 @@ RSpec.describe Form::Lettings::Questions::ManagingOrganisation, type: :model do context "when organisation has merged" do let(:absorbing_org) { create(:organisation, name: "Absorbing org", holds_own_stock: true) } let!(:merged_org) { create(:organisation, name: "Merged org", holds_own_stock: false) } - let!(:merged_deleted_org) { create(:organisation, name: "Merged org", holds_own_stock: false, discarded_at: Time.zone.yesterday) } + let!(:merged_deleted_org) { create(:organisation, name: "Merged org 2", holds_own_stock: false, discarded_at: Time.zone.yesterday) } let(:user) { create(:user, :data_coordinator, organisation: absorbing_org) } let(:log) do diff --git a/spec/models/form/sales/questions/buyer2_working_situation_spec.rb b/spec/models/form/sales/questions/buyer2_working_situation_spec.rb index 7b825185c..c8ee349b6 100644 --- a/spec/models/form/sales/questions/buyer2_working_situation_spec.rb +++ b/spec/models/form/sales/questions/buyer2_working_situation_spec.rb @@ -36,6 +36,22 @@ RSpec.describe Form::Sales::Questions::Buyer2WorkingSituation, type: :model do "0" => { "value" => "Other" }, "10" => { "value" => "Buyer prefers not to say" }, "7" => { "value" => "Full-time student" }, + "9" => { "value" => "Child under 16" }, + }) + end + + it "has the correct displayed_answer_options" do + expect(question.displayed_answer_options(nil)).to eq({ + "1" => { "value" => "Full-time - 30 hours or more" }, + "2" => { "value" => "Part-time - Less than 30 hours" }, + "3" => { "value" => "In government training into work" }, + "4" => { "value" => "Jobseeker" }, + "6" => { "value" => "Not seeking work" }, + "8" => { "value" => "Unable to work due to long term sick or disability" }, + "5" => { "value" => "Retired" }, + "0" => { "value" => "Other" }, + "10" => { "value" => "Buyer prefers not to say" }, + "7" => { "value" => "Full-time student" }, }) end @@ -43,7 +59,11 @@ RSpec.describe Form::Sales::Questions::Buyer2WorkingSituation, type: :model do let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1), start_year_2025_or_later?: false) } it "uses the old ordering for answer options" do - expect(question.answer_options.keys).to eq(%w[1 2 3 4 6 8 5 0 10 7]) + expect(question.answer_options.keys).to eq(%w[1 2 3 4 6 8 5 0 10 7 9]) + end + + it "uses the old ordering for displayed answer options" do + expect(question.displayed_answer_options(nil).keys).to eq(%w[1 2 3 4 6 8 5 0 10 7]) end end @@ -51,7 +71,11 @@ RSpec.describe Form::Sales::Questions::Buyer2WorkingSituation, type: :model do let(:form) { instance_double(Form, start_date: Time.zone.local(2025, 4, 1), start_year_2025_or_later?: true) } it "uses the new ordering for answer options" do - expect(question.answer_options.keys).to eq(%w[1 2 3 4 5 6 7 8 0 10]) + expect(question.answer_options.keys).to eq(%w[1 2 3 4 5 6 7 8 9 0 10]) + end + + it "uses the new ordering for displayed answer options" do + expect(question.displayed_answer_options(nil).keys).to eq(%w[1 2 3 4 5 6 7 8 0 10]) end end diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index 9b01845ae..b117feef7 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -19,6 +19,12 @@ RSpec.describe Organisation, type: :model do .to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Provider type #{I18n.t('validations.organisation.provider_type_missing')}") end + it "validates uniqueness of name" do + org = build(:organisation, name: organisation.name.downcase) + org.valid? + expect(org.errors[:name]).to include(I18n.t("validations.organisation.name_not_unique")) + end + context "with parent/child associations", :aggregate_failures do let!(:child_organisation) { create(:organisation, name: "MHCLG Child") } let!(:grandchild_organisation) { create(:organisation, name: "MHCLG Grandchild") } diff --git a/spec/models/scheme_spec.rb b/spec/models/scheme_spec.rb index 65174388d..21b60cd52 100644 --- a/spec/models/scheme_spec.rb +++ b/spec/models/scheme_spec.rb @@ -390,6 +390,13 @@ RSpec.describe Scheme, type: :model do scheme.startdate = Time.zone.today + 2.weeks expect(scheme.status).to eq(:activating_soon) end + + it "returns deactivated if scheme is deactivated and incomplete" do + scheme.update!(support_type: nil, confirmed: nil) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.yesterday, scheme:) + scheme.reload + expect(scheme.status).to eq(:deactivated) + end end context "when there have been previous deactivations" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 51cfc00bd..53561f3e9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -300,6 +300,7 @@ RSpec.describe User, type: :model do context "when the user is in staging environment" do before do allow(Rails.env).to receive(:staging?).and_return(true) + allow(Rails.application.credentials).to receive(:[]).with(:staging_role_update_email_allowlist).and_return(["not_one_of_the_examples.com"]) end context "and the user is not in the staging role update email allowlist" do diff --git a/spec/requests/bulk_upload_lettings_logs_controller_spec.rb b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb index 18b208e74..c9a22768d 100644 --- a/spec/requests/bulk_upload_lettings_logs_controller_spec.rb +++ b/spec/requests/bulk_upload_lettings_logs_controller_spec.rb @@ -73,5 +73,116 @@ RSpec.describe BulkUploadLettingsLogsController, type: :request do expect(response.body).to include("How to upload logs in bulk") end end + + context "when no year is specified" do + it "shows guidance page with links defaulting to the current year" do + get "/lettings-logs/bulk-upload-logs/guidance" + + expect(response.body).to include("Download the lettings bulk upload template (#{current_collection_start_year} to #{current_collection_start_year + 1})") + end + end + + context "when an invalid year is specified" do + it "shows not found" do + get "/lettings-logs/bulk-upload-logs/guidance?form%5Byear%5D=10000" + + expect(response).to be_not_found + end + end + end + + describe "GET /lettings-logs/bulk-upload-logs/year" do + it "does not require a year to be specified" do + get "/lettings-logs/bulk-upload-logs/year" + + expect(response).to be_ok + end + end + + pages_requiring_year_specification = %w[prepare-your-file upload-your-file checking-file] + pages_requiring_year_specification.each do |page_id| + describe "GET /lettings-logs/bulk-upload-logs/#{page_id}" do + context "when no year is provided" do + it "returns not found" do + get "/lettings-logs/bulk-upload-logs/#{page_id}" + + expect(response).to be_not_found + end + end + + context "when requesting the previous year in a crossover period" do + before do + allow(FormHandler.instance).to receive(:lettings_in_crossover_period?).and_return(true) + end + + it "succeeds" do + get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}" + + expect(response).to be_ok + end + end + + context "when requesting the previous year outside a crossover period" do + before do + allow(FormHandler.instance).to receive(:lettings_in_crossover_period?).and_return(false) + end + + it "returns not found" do + get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}" + + expect(response).to be_not_found + end + end + + context "when requesting the current year" do + it "succeeds" do + get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year}" + + expect(response).to be_ok + end + end + + if page_id != "prepare-your-file" + context "when requesting the next year with future form use toggled on" do + before do + allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(true) + end + + it "succeeds" do + get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}" + + expect(response).to be_ok + end + end + end + + context "when requesting the next year with future form use toggled off" do + before do + allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(false) + end + + it "returns not found" do + get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}" + + expect(response).to be_not_found + end + end + + context "when requesting a far future year" do + it "returns not found" do + get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=9990" + + expect(response).to be_not_found + end + end + + context "when requesting a nonsense value for year" do + it "returns not found" do + get "/lettings-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=thisisnotayear" + + expect(response).to be_not_found + end + end + end end end diff --git a/spec/requests/bulk_upload_sales_logs_controller_spec.rb b/spec/requests/bulk_upload_sales_logs_controller_spec.rb index 7cd6d4be8..4c20482be 100644 --- a/spec/requests/bulk_upload_sales_logs_controller_spec.rb +++ b/spec/requests/bulk_upload_sales_logs_controller_spec.rb @@ -73,5 +73,116 @@ RSpec.describe BulkUploadSalesLogsController, type: :request do expect(response.body).to include("How to upload logs in bulk") end end + + context "when no year is specified" do + it "shows guidance page with links defaulting to the current year" do + get "/sales-logs/bulk-upload-logs/guidance" + + expect(response.body).to include("Download the sales bulk upload template (#{current_collection_start_year} to #{current_collection_start_year + 1})") + end + end + + context "when an invalid year is specified" do + it "shows not found" do + get "/sales-logs/bulk-upload-logs/guidance?form%5Byear%5D=10000" + + expect(response).to be_not_found + end + end + end + + describe "GET /sales-logs/bulk-upload-logs/year" do + it "does not require a year to be specified" do + get "/sales-logs/bulk-upload-logs/year" + + expect(response).to be_ok + end + end + + pages_requiring_year_specification = %w[prepare-your-file upload-your-file checking-file] + pages_requiring_year_specification.each do |page_id| + describe "GET /sales-logs/bulk-upload-logs/#{page_id}" do + context "when no year is provided" do + it "returns not found" do + get "/sales-logs/bulk-upload-logs/#{page_id}" + + expect(response).to be_not_found + end + end + + context "when requesting the previous year in a crossover period" do + before do + allow(FormHandler.instance).to receive(:sales_in_crossover_period?).and_return(true) + end + + it "succeeds" do + get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}" + + expect(response).to be_ok + end + end + + context "when requesting the previous year outside a crossover period" do + before do + allow(FormHandler.instance).to receive(:sales_in_crossover_period?).and_return(false) + end + + it "returns not found" do + get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year - 1}" + + expect(response).to be_not_found + end + end + + context "when requesting the current year" do + it "succeeds" do + get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year}" + + expect(response).to be_ok + end + end + + if page_id != "prepare-your-file" + context "when requesting the next year with future form use toggled on" do + before do + allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(true) + end + + it "succeeds" do + get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}" + + expect(response).to be_ok + end + end + end + + context "when requesting the next year with future form use toggled off" do + before do + allow(FeatureToggle).to receive(:allow_future_form_use?).and_return(false) + end + + it "returns not found" do + get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=#{current_collection_start_year + 1}" + + expect(response).to be_not_found + end + end + + context "when requesting a far future year" do + it "returns not found" do + get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=9990" + + expect(response).to be_not_found + end + end + + context "when requesting a nonsense value for year" do + it "returns not found" do + get "/sales-logs/bulk-upload-logs/#{page_id}?form%5Byear%5D=thisisnotayear" + + expect(response).to be_not_found + end + end + end end end diff --git a/spec/requests/check_errors_controller_spec.rb b/spec/requests/check_errors_controller_spec.rb index 29130f547..9c4d3f8c1 100644 --- a/spec/requests/check_errors_controller_spec.rb +++ b/spec/requests/check_errors_controller_spec.rb @@ -84,7 +84,7 @@ RSpec.describe CheckErrorsController, type: :request do end 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_button("Clear all") end diff --git a/spec/requests/lettings_logs_controller_spec.rb b/spec/requests/lettings_logs_controller_spec.rb index e795b76ea..d84a6d714 100644 --- a/spec/requests/lettings_logs_controller_spec.rb +++ b/spec/requests/lettings_logs_controller_spec.rb @@ -759,7 +759,7 @@ RSpec.describe LettingsLogsController, type: :request do it "has search results in the title" do get "/lettings-logs?search=#{log_to_search.id}", headers:, params: {} - expect(page).to have_title("Lettings logs (1 logs matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") + expect(page).to have_title("Lettings logs (1 log matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") end it "shows lettings logs matching the id" do @@ -895,7 +895,7 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows the total log count" do - expect(CGI.unescape_html(response.body)).to match("1 total logs") + expect(CGI.unescape_html(response.body)).to match("1 total log") end it "does not show the pagination links" do diff --git a/spec/requests/organisation_relationships_controller_spec.rb b/spec/requests/organisation_relationships_controller_spec.rb index 0df6fbe36..1b1b3169e 100644 --- a/spec/requests/organisation_relationships_controller_spec.rb +++ b/spec/requests/organisation_relationships_controller_spec.rb @@ -53,12 +53,12 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 total stock owners") + expect(page).to have_content("1 total stock owner") end context "when adding a stock owner" do let!(:active_organisation) { FactoryBot.create(:organisation, name: "Active Org", active: true) } - let!(:inactive_organisation) { FactoryBot.create(:organisation, name: "Inactive LTD", active: false) } + let!(:inactive_organisation) { FactoryBot.create(:organisation, name: "Inactive LTD 2", active: false) } before do get "/organisations/#{organisation.id}/stock-owners/add", headers:, params: {} @@ -115,7 +115,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do let!(:managing_agent) { FactoryBot.create(:organisation) } let!(:other_org_managing_agent) { FactoryBot.create(:organisation, name: "Foobar LTD") } let!(:inactive_managing_agent) { FactoryBot.create(:organisation, name: "Inactive LTD", active: false) } - let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") } + let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 3") } before do FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent) @@ -149,7 +149,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 total managing agents") + expect(page).to have_content("1 total managing agent") end context "and current organisation is deactivated" do @@ -316,7 +316,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do context "with an organisation that the user belongs to" do let!(:stock_owner) { FactoryBot.create(:organisation) } let!(:other_org_stock_owner) { FactoryBot.create(:organisation, name: "Foobar LTD") } - let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") } + let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 2") } before do FactoryBot.create(:organisation_relationship, child_organisation: organisation, parent_organisation: stock_owner) @@ -345,7 +345,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 total stock owners") + expect(page).to have_content("1 total stock owner") end end @@ -452,7 +452,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do context "with an organisation that the user belongs to" do let!(:managing_agent) { FactoryBot.create(:organisation) } let!(:other_org_managing_agent) { FactoryBot.create(:organisation, name: "Foobar LTD") } - let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD") } + let!(:other_organisation) { FactoryBot.create(:organisation, name: "Foobar LTD 5") } before do FactoryBot.create(:organisation_relationship, parent_organisation: organisation, child_organisation: managing_agent) @@ -481,7 +481,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 total managing agents") + expect(page).to have_content("1 total managing agent") end end @@ -647,7 +647,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 total stock owners") + expect(page).to have_content("1 total stock owner") end context "when adding a stock owner" do @@ -697,7 +697,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 total managing agents") + expect(page).to have_content("1 total managing agent") end it "shows remove link(s)" do diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index a9cad2d84..795ab5ab6 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -1203,7 +1203,7 @@ RSpec.describe OrganisationsController, type: :request do it "has search results in the title" do 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 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 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 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 } before do - create_list(:organisation, 25) + build_list(:organisation, 25) do |organisation, index| + organisation.name = "Organisation #{index}" + organisation.save! + end get "/organisations" end @@ -1616,11 +1619,11 @@ RSpec.describe OrganisationsController, type: :request do end 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 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 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" } 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}" end diff --git a/spec/requests/sales_logs_controller_spec.rb b/spec/requests/sales_logs_controller_spec.rb index dc056c1ed..6c882cc87 100644 --- a/spec/requests/sales_logs_controller_spec.rb +++ b/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 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 it "shows sales logs matching the id" do @@ -692,7 +692,7 @@ RSpec.describe SalesLogsController, type: :request do end it "shows the total log count" do - expect(CGI.unescape_html(response.body)).to match("1 total logs") + expect(CGI.unescape_html(response.body)).to match("1 total log") end it "does not show the pagination links" do diff --git a/spec/services/bulk_upload/lettings/validator_spec.rb b/spec/services/bulk_upload/lettings/validator_spec.rb index 897010de6..60eb8a955 100644 --- a/spec/services/bulk_upload/lettings/validator_spec.rb +++ b/spec/services/bulk_upload/lettings/validator_spec.rb @@ -232,7 +232,7 @@ RSpec.describe BulkUpload::Lettings::Validator do end end - describe "#create_logs?" do + describe "#block_log_creation_reason" 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_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 validator.call - expect(validator).to be_create_logs + expect(validator.block_log_creation_reason).to be_nil end end @@ -261,7 +261,7 @@ RSpec.describe BulkUpload::Lettings::Validator do it "returns true" do validator.call - expect(validator).to be_create_logs + expect(validator.block_log_creation_reason).to be_nil end end @@ -277,7 +277,7 @@ RSpec.describe BulkUpload::Lettings::Validator do it "will not create logs" do validator.call - expect(validator).not_to be_create_logs + expect(validator.block_log_creation_reason).to eq("setup_errors") end end @@ -291,7 +291,7 @@ RSpec.describe BulkUpload::Lettings::Validator do it "returns false" do validator.call - expect(validator).not_to be_create_logs + expect(validator.block_log_creation_reason).to eq("setup_errors") end end end diff --git a/spec/services/bulk_upload/processor_spec.rb b/spec/services/bulk_upload/processor_spec.rb index de0ed2dba..0368635e7 100644 --- a/spec/services/bulk_upload/processor_spec.rb +++ b/spec/services/bulk_upload/processor_spec.rb @@ -14,7 +14,7 @@ RSpec.describe BulkUpload::Processor do call: nil, total_logs_count: 1, any_setup_errors?: false, - create_logs?: true, + block_log_creation_reason: nil, soft_validation_errors_only?: false, ) end @@ -165,7 +165,7 @@ RSpec.describe BulkUpload::Processor do let(:log) { build(:lettings_log, :setup_completed, assigned_to: user) } 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(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) end @@ -198,7 +198,7 @@ RSpec.describe BulkUpload::Processor do context "when a bulk upload has logs with only soft validations triggered" 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(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) end @@ -239,7 +239,7 @@ RSpec.describe BulkUpload::Processor do call: nil, total_logs_count: 1, any_setup_errors?: false, - create_logs?: false, + block_log_creation_reason: "row_parser_block_log_creation", ) end @@ -254,6 +254,30 @@ RSpec.describe BulkUpload::Processor do expect(mail_double).to have_received(:deliver_later) 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 describe "#approve" do diff --git a/spec/services/bulk_upload/sales/validator_spec.rb b/spec/services/bulk_upload/sales/validator_spec.rb index c275ce681..968014e7c 100644 --- a/spec/services/bulk_upload/sales/validator_spec.rb +++ b/spec/services/bulk_upload/sales/validator_spec.rb @@ -204,7 +204,7 @@ RSpec.describe BulkUpload::Sales::Validator do end end - describe "#create_logs?" do + describe "#block_log_creation_reason" do context "when all logs are valid" do let(:log_1) { 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) end - it "returns truthy" do + it "returns nil" do validator.call - expect(validator).to be_create_logs + expect(validator.block_log_creation_reason).to be_nil end end @@ -229,9 +229,9 @@ RSpec.describe BulkUpload::Sales::Validator do file.write(BulkUpload::SalesLogToCsv.new(log: log_2).to_csv_row) end - it "returns truthy" do + it "returns nil" do validator.call - expect(validator).to be_create_logs + expect(validator.block_log_creation_reason).to be_nil end end @@ -245,9 +245,9 @@ RSpec.describe BulkUpload::Sales::Validator do file.close end - it "returns false" do + it "returns the reason" do validator.call - expect(validator).not_to be_create_logs + expect(validator.block_log_creation_reason).to eq("setup_errors") end end @@ -262,7 +262,7 @@ RSpec.describe BulkUpload::Sales::Validator do it "will not create logs" do validator.call - expect(validator).not_to be_create_logs + expect(validator.block_log_creation_reason).to eq("setup_errors") end end end diff --git a/spec/services/exports/lettings_log_export_service_spec.rb b/spec/services/exports/lettings_log_export_service_spec.rb index 8c123b47e..c0dde5771 100644 --- a/spec/services/exports/lettings_log_export_service_spec.rb +++ b/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) 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_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_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!(/\{scheme_id\}/, (lettings_log["scheme_id"]).to_s) if lettings_log.needstype == 2 export_template.sub!(/\{log_id\}/, lettings_log["id"].to_s) diff --git a/spec/services/exports/organisation_export_service_spec.rb b/spec/services/exports/organisation_export_service_spec.rb index 43ca19095..51c8fe8cf 100644 --- a/spec/services/exports/organisation_export_service_spec.rb +++ b/spec/services/exports/organisation_export_service_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Exports::OrganisationExportService do def replace_entity_ids(organisation, export_template) 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!(/\{dpo_email\}/, organisation.data_protection_confirmation&.data_protection_officer_email) end diff --git a/spec/services/exports/user_export_service_spec.rb b/spec/services/exports/user_export_service_spec.rb index 8a0e22267..854dd1ce7 100644 --- a/spec/services/exports/user_export_service_spec.rb +++ b/spec/services/exports/user_export_service_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Exports::UserExportService do def replace_entity_ids(user, export_template) export_template.sub!(/\{id\}/, user["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) end diff --git a/yarn.lock b/yarn.lock index 6b1579e64..483095046 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4335,9 +4335,9 @@ mute-stream@0.0.8: integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== natural-compare@^1.4.0: version "1.4.0"