From 6917e0bf1ccd6f343dc8fad8fc431702399a7fd3 Mon Sep 17 00:00:00 2001 From: Reem Ibrahim Date: Thu, 13 Aug 2026 14:26:55 +0100 Subject: [PATCH 1/5] updates relating to self serve user deactivation --- app/controllers/v1/users_controller.rb | 18 ++++++ app/models/user.rb | 6 ++ app/serializable/serializable_user.rb | 2 +- app/services/deactivate_user.rb | 19 ++++++- config/routes.rb | 1 + spec/models/offboard/deactivate_users_spec.rb | 1 + spec/models/user_spec.rb | 51 +++++++++++++++++ spec/requests/v1/users_spec.rb | 57 +++++++++++++++++++ spec/services/deactivate_user_spec.rb | 25 ++++++++ 9 files changed, 178 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 3ef853c73..82c2486ae 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -73,4 +73,22 @@ def user_auth_logs render jsonapi: objects, class: { OpenStruct: SerializableUserAuthLog }, status: :ok end + + def deactivate + user = User.find_by!(auth_id: current_auth_id) + + unless user.can_deactivate? + return render jsonapi_errors: { user: ['Cannot be deactivated because they are the only user associated with their suppliers'] }, + status: :unprocessable_entity + end + + result = DeactivateUser.new(user: user).call + + if result.success? + render jsonapi: user, status: :ok + else + render jsonapi_errors: { user: ['Could not be deactivated'] }, + status: :unprocessable_entity + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 32cba1e76..e4e08b89b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,4 +40,10 @@ def multiple_suppliers? def active? !auth_id.nil? end + + def can_deactivate? + suppliers.all? do |supplier| + supplier.active_users.where.not(id: id).exists? + end + end end diff --git a/app/serializable/serializable_user.rb b/app/serializable/serializable_user.rb index 14da8dae9..080926e1d 100644 --- a/app/serializable/serializable_user.rb +++ b/app/serializable/serializable_user.rb @@ -1,4 +1,4 @@ class SerializableUser < JSONAPI::Serializable::Resource type 'users' - attributes :multiple_suppliers?, :name, :email, :created_at + attributes :multiple_suppliers?, :can_deactivate?, :name, :email, :created_at end diff --git a/app/services/deactivate_user.rb b/app/services/deactivate_user.rb index d62e20bd7..338b917f8 100644 --- a/app/services/deactivate_user.rb +++ b/app/services/deactivate_user.rb @@ -9,6 +9,13 @@ def call result = Result.new(true) User.transaction do + lock_linked_suppliers! + + unless user.can_deactivate? + result.success = false + raise ActiveRecord::Rollback + end + begin DeleteUserInAuth0.new(user: user).call rescue Auth0::Exception @@ -16,9 +23,19 @@ def call Rails.logger.error("Error adding user #{user.email} to Auth0 during DeactivateUser") raise ActiveRecord::Rollback end - user.update(auth_id: nil) + + unless user.update(auth_id: nil) + result.success = false + raise ActiveRecord::Rollback + end end result end + + private + + def lock_linked_suppliers! + user.suppliers.lock.load + end end diff --git a/config/routes.rb b/config/routes.rb index 9a8cdf107..fb51d4dea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,7 @@ collection do patch :update_name patch :update_email + patch :deactivate get :user_auth_logs end end diff --git a/spec/models/offboard/deactivate_users_spec.rb b/spec/models/offboard/deactivate_users_spec.rb index 4d9e8e97f..bdcf54a11 100644 --- a/spec/models/offboard/deactivate_users_spec.rb +++ b/spec/models/offboard/deactivate_users_spec.rb @@ -6,6 +6,7 @@ end let!(:user) { FactoryBot.create(:user, name: 'User One', email: 'email_one@ccs.co.uk', suppliers: [supplier]) } + let!(:user_two) { FactoryBot.create(:user, name: 'User Two', email: 'email_two@ccs.co.uk', suppliers: [supplier]) } before do stub_auth0_token_request diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 21f23eb59..2fdce7623 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -53,6 +53,57 @@ end end + describe '#can_deactivate?' do + subject(:can_deactivate?) { user.can_deactivate? } + + let(:user) { FactoryBot.create(:user) } + + context 'when the user is the only active user for a supplier' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + end + + it { is_expected.to be_falsy } + end + + context 'when the user is not the only active user for a supplier' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user) + other_user.suppliers << supplier + end + + it { is_expected.to be_truthy } + end + + context 'when another linked user is inactive' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user, :inactive) + other_user.suppliers << supplier + end + + it { is_expected.to be_falsy } + end + + context 'when one supplier has multiple active users and another has only one' do + before do + supplier1 = FactoryBot.create(:supplier) + user.suppliers << supplier1 + other_user1 = FactoryBot.create(:user) + other_user1.suppliers << supplier1 + + supplier2 = FactoryBot.create(:supplier) + user.suppliers << supplier2 + end + + it { is_expected.to be_falsy } + end + end + describe '.search' do let!(:bob) { FactoryBot.create(:user, name: 'Bob Booker', email: 'bob@sheffield.com') } let!(:bobby) { FactoryBot.create(:user, name: 'Bobby Brown', email: 'bobby_b_66@hotmail.com') } diff --git a/spec/requests/v1/users_spec.rb b/spec/requests/v1/users_spec.rb index 2d52dc755..2e66cf141 100644 --- a/spec/requests/v1/users_spec.rb +++ b/spec/requests/v1/users_spec.rb @@ -16,6 +16,8 @@ expect(json['data'][0]) .to have_attribute(:multiple_suppliers?) .with_value(false) + expect(json['data'][0]) + .to have_attribute(:can_deactivate?) end it 'returns the details of the current user who belongs to more than one supplier' do @@ -30,6 +32,22 @@ .to have_attribute(:multiple_suppliers?) .with_value(true) end + + it 'returns that the user can be deactivated if they are not the only active user for a supplier' do + user = FactoryBot.create(:user) + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user) + other_user.suppliers << supplier + + get '/v1/users', headers: { 'X-Auth-Id' => JWT.encode(user.auth_id, 'test') } + + expect(json['data'].size).to eql 1 + expect(response).to be_successful + expect(json['data'][0]) + .to have_attribute(:can_deactivate?) + .with_value(true) + end end describe 'PATCH /v1/users/update_name' do @@ -125,4 +143,43 @@ expect(response.status).to eq 200 end end + + describe 'PATCH /v1/users/deactivate' do + let(:user) { FactoryBot.create(:user) } + let(:headers) { { 'X-Auth-Id' => JWT.encode(user.auth_id, 'test') } } + + context 'when the user can be deactivated' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user) + other_user.suppliers << supplier + end + + it 'deactivates the user' do + stub_auth0_token_request + stub_auth0_delete_user_request(user) + + patch '/v1/users/deactivate', headers: headers + + expect(response).to be_successful + expect(user.reload.auth_id).to be_nil + end + end + + context 'when the user cannot be deactivated' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + end + + it 'returns an error' do + patch '/v1/users/deactivate', headers: headers + + expect(response.status).to eq 422 + expect(json['errors']).not_to be_empty + expect(user.reload.auth_id).not_to be_nil + end + end + end end diff --git a/spec/services/deactivate_user_spec.rb b/spec/services/deactivate_user_spec.rb index 870320a68..461706b21 100644 --- a/spec/services/deactivate_user_spec.rb +++ b/spec/services/deactivate_user_spec.rb @@ -1,8 +1,13 @@ require 'rails_helper' RSpec.describe DeactivateUser do + let(:suppler) { create(:supplier) } let(:user) { create(:user) } + let(:other_user) { create(:user) } + before(:each) do + user.suppliers << suppler + other_user.suppliers << suppler stub_auth0_token_request end @@ -16,6 +21,26 @@ expect(result.failure?).to eq(false) end + context 'when the user is the only active user for a supplier' do + let(:other_user) { create(:user, :inactive) } + + it 'returns a failed result' do + result = described_class.new(user: user).call + expect(result).to be_failure + end + + it 'does not delete the user in Auth0' do + expect_any_instance_of(DeleteUserInAuth0).not_to receive(:call) + described_class.new(user: user).call + end + + it 'does not clear the auth_id of the user' do + original_auth_id = user.auth_id + described_class.new(user: user).call + expect(user.auth_id).to eql(original_auth_id) + end + end + context 'when Auth0 errors' do before(:each) do stub_auth0_delete_user_request_failure(user) From 33649468a28e7efce214bc1b120d10878fc34ddd Mon Sep 17 00:00:00 2001 From: Reem Ibrahim Date: Thu, 13 Aug 2026 14:44:18 +0100 Subject: [PATCH 2/5] rubocop --- app/controllers/v1/users_controller.rb | 4 +++- app/services/deactivate_user.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 82c2486ae..1bdf8c292 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -78,8 +78,10 @@ def deactivate user = User.find_by!(auth_id: current_auth_id) unless user.can_deactivate? + # rubocop:disable Layout/LineLength return render jsonapi_errors: { user: ['Cannot be deactivated because they are the only user associated with their suppliers'] }, - status: :unprocessable_entity + status: :unprocessable_entity + # rubocop:enable Layout/LineLength end result = DeactivateUser.new(user: user).call diff --git a/app/services/deactivate_user.rb b/app/services/deactivate_user.rb index 338b917f8..935beae87 100644 --- a/app/services/deactivate_user.rb +++ b/app/services/deactivate_user.rb @@ -36,6 +36,6 @@ def call private def lock_linked_suppliers! - user.suppliers.lock.load + user.suppliers.lock.load end end From bd453b9ec83db85067ae22291bade40cfee303f3 Mon Sep 17 00:00:00 2001 From: Reem Ibrahim Date: Thu, 20 Aug 2026 09:31:13 +0100 Subject: [PATCH 3/5] added searchable inactive urns and updated specs --- app/controllers/admin/urns_controller.rb | 18 +++++- app/models/inactive_customer.rb | 15 +++++ app/views/admin/urn_lists/index.html.haml | 2 +- .../admin/urns/_active_customers.html.haml | 49 +++++++++++++++ .../admin/urns/_inactive_customers.html.haml | 50 ++++++++++++++++ app/views/admin/urns/index.html.haml | 60 ++++++------------- app/views/admin/urns/index.js.haml | 8 +++ spec/features/admin_can_search_urns_spec.rb | 12 ++-- spec/models/inactive_customer_spec.rb | 47 +++++++++++++++ spec/requests/admin/urns_spec.rb | 54 +++++++++++++++++ 10 files changed, 265 insertions(+), 50 deletions(-) create mode 100644 app/views/admin/urns/_active_customers.html.haml create mode 100644 app/views/admin/urns/_inactive_customers.html.haml create mode 100644 app/views/admin/urns/index.js.haml diff --git a/app/controllers/admin/urns_controller.rb b/app/controllers/admin/urns_controller.rb index 7841235fe..caa4a8541 100644 --- a/app/controllers/admin/urns_controller.rb +++ b/app/controllers/admin/urns_controller.rb @@ -2,9 +2,23 @@ class Admin::UrnsController < AdminController def index - @search = params[:search].to_s.strip + @active_search = params[:active_search].to_s.strip + @inactive_search = params[:inactive_search].to_s.strip - @customers = Customer.where(deleted: false).order(:name).search(@search).page(params[:page]) + @customers = Customer + .where(deleted: false) + .search(@active_search) + .order(:name) + .page(params[:active_page]) + @inactive_customers = InactiveCustomer + .search(@inactive_search) + .order(date_made_inactive: :desc) + .page(params[:inactive_page]) + + respond_to do |format| + format.html + format.js + end end def download diff --git a/app/models/inactive_customer.rb b/app/models/inactive_customer.rb index 84080ff41..5690cfb9f 100644 --- a/app/models/inactive_customer.rb +++ b/app/models/inactive_customer.rb @@ -1,3 +1,18 @@ class InactiveCustomer < ApplicationRecord validates :inactive_urn, presence: true, uniqueness: true + + def self.search(query) + if query.blank? + all + else + where( + 'cast(inactive_urn as text) ILIKE :query + OR inactive_customer_name ILIKE :query + OR cast(replacement_urn as text) ILIKE :query + OR replacement_customer_name ILIKE :query + OR replacement_post_code ILIKE :query', + query: "%#{query}%" + ) + end + end end diff --git a/app/views/admin/urn_lists/index.html.haml b/app/views/admin/urn_lists/index.html.haml index ee3a59988..27d643ca9 100644 --- a/app/views/admin/urn_lists/index.html.haml +++ b/app/views/admin/urn_lists/index.html.haml @@ -10,7 +10,7 @@ %li.govuk-page-actions--action = link_to 'Add a new Active URN list', new_admin_urn_list_path %li.govuk-page-actions--action - = link_to 'View Active URN list', admin_urns_path + = link_to 'View URN lists', admin_urns_path .govuk-grid-row .govuk-grid-column-full diff --git a/app/views/admin/urns/_active_customers.html.haml b/app/views/admin/urns/_active_customers.html.haml new file mode 100644 index 000000000..761b48080 --- /dev/null +++ b/app/views/admin/urns/_active_customers.html.haml @@ -0,0 +1,49 @@ +.govuk-grid-row + .govuk-grid-column-two-thirds + %h3.govuk-heading-s Search + + = form_with url: admin_urns_path, method: :get, local: false do + .ccs-search-form-group + %label.govuk-label.govuk-visually-hidden{for: 'active-search'} + Search active URNs + %input#active-search{ + name: 'active_search', + type: 'text', + value: @active_search, + class: ['govuk-!-width-two-thirds', 'govuk-input'] + } + %button#active-search-button.govuk-button Search + + .govuk-grid-column-one-third + %nav.govuk-page-actions{"aria-labelledby" => "page-actions-title"} + %h2#page-actions-title.govuk-heading-s{"aria-label" => "Page actions"} Actions + %ul.govuk-page-actions--actions + %li.govuk-page-actions--action + = link_to 'Download Active URN list', download_admin_urns_path + +.govuk-grid-row + .govuk-grid-column-full + - if @customers.any? + %table.govuk-table{:class => 'govuk-!-margin-top-7'} + %thead.govuk-table__head + %tr.govuk-table__row + %th.govuk-table__header URN + %th.govuk-table__header Organisation name + %th.govuk-table__header Sector + %th.govuk-table__header Postcode + %th.govuk-table__header Published + %tbody.govuk-table__body + - @customers.each do |customer| + %tr.govuk-table__row + %td.govuk-table__cell= customer.urn + %td.govuk-table__cell= customer.name + %td.govuk-table__cell= customer.sector.titleize + %td.govuk-table__cell= customer.postcode + %td.govuk-table__cell= customer.published? ? 'true' : 'false' + %nav.pagination.ccs-pagination{"aria-label" => "Active URN pagination", :role => "navigation"} + #active-customers-pagination-summary.ccs-pagination__summary= page_entries_info @customers, entry_name: "customer" + #active-customers-pagination= paginate @customers, param_name: :active_page, params: {active_search: @active_search}, remote: true + + - elsif @active_search.present? + %p + No customers found for ‘#{@active_search}’. \ No newline at end of file diff --git a/app/views/admin/urns/_inactive_customers.html.haml b/app/views/admin/urns/_inactive_customers.html.haml new file mode 100644 index 000000000..a5118d3e9 --- /dev/null +++ b/app/views/admin/urns/_inactive_customers.html.haml @@ -0,0 +1,50 @@ +.govuk-grid-row + .govuk-grid-column-two-thirds + %h3.govuk-heading-s Search + + = form_with url: admin_urns_path, method: :get, local: false do + .ccs-search-form-group + %label.govuk-label.govuk-visually-hidden{for: 'inactive-search'} + Search inactive URNs + %input#inactive-search{ + name: 'inactive_search', + type: 'text', + value: @inactive_search, + class: ['govuk-!-width-two-thirds', 'govuk-input'] + } + %button#inactive-search-button.govuk-button Search + +.govuk-grid-row + .govuk-grid-column-full + - if @inactive_customers.any? + %table.govuk-table{:class => 'govuk-!-margin-top-7'} + %thead.govuk-table__head + %tr.govuk-table__row + %th.govuk-table__header Inactive Customer + %th.govuk-table__header Inactive Date + %th.govuk-table__header Replacement Customer + %th.govuk-table__header Post Code + %th.govuk-table__header Replacement Status + %tbody.govuk-table__body + - @inactive_customers.each do |inactive_customer| + %tr.govuk-table__row + %td.govuk-table__cell + = inactive_customer.inactive_customer_name + %br/ + %small + = inactive_customer.inactive_urn + %td.govuk-table__cell= inactive_customer.date_made_inactive + %td.govuk-table__cell + = inactive_customer.replacement_customer_name + %br/ + %small + = inactive_customer.replacement_urn + %td.govuk-table__cell= inactive_customer.replacement_post_code + %td.govuk-table__cell= inactive_customer.replacement_status + %nav.pagination.ccs-pagination{"aria-label" => "Inactive URN pagination", :role => "navigation"} + #inactive-customers-pagination-summary.ccs-pagination__summary= page_entries_info @inactive_customers, entry_name: "inactive customer" + #inactive-customers-pagination= paginate @inactive_customers, param_name: :inactive_page, params: {active_search: @inactive_search}, remote: true + + - elsif @inactive_search.present? + %p + No inactive customers found for ‘#{@inactive_search}’. \ No newline at end of file diff --git a/app/views/admin/urns/index.html.haml b/app/views/admin/urns/index.html.haml index 6ac9eabef..b3eff483c 100644 --- a/app/views/admin/urns/index.html.haml +++ b/app/views/admin/urns/index.html.haml @@ -2,48 +2,26 @@ .govuk-grid-column-two-thirds = link_to 'Back', admin_urn_lists_path, { class: 'govuk-back-link govuk-!-margin-bottom-5', title: 'Back to URN list log' } - %h1.govuk-heading-xl Active URN list + %h1.govuk-heading-xl URN lists -.govuk-grid-row - .govuk-grid-column-two-thirds - %h2.govuk-heading-s - Search - = form_with url: admin_urns_path, method: :get, local: true do - .ccs-search-form-group - %label.govuk-label.govuk-visually-hidden{for: 'search'} Search - %input#search{name: 'search', type: 'text', value: params[:search], class: ['govuk-!-width-two-thirds', 'govuk-input']} - %button.govuk-button Search +.govuk-tabs{data: {module: 'govuk-tabs'}} + %h2.govuk-tabs__title + Contents - .govuk-grid-column-one-third - %nav.govuk-page-actions{"aria-labelledby" => "page-actions-title"} - %h2#page-actions-title.govuk-heading-s{"aria-label" => "Page actions"} Actions - %ul.govuk-page-actions--actions - %li.govuk-page-actions--action - = link_to 'Download Active URN list', download_admin_urns_path + %ul.govuk-tabs__list + %li.govuk-tabs__list-item.govuk-tabs__list-item--selected + %a.govuk-tabs__tab{href: '#active-urns'} + Active URNs -.govuk-grid-row - .govuk-grid-column-full - - if @customers.any? - %table.govuk-table{:class => 'govuk-!-margin-top-7'} - %thead.govuk-table__head - %tr.govuk-table__row - %th.govuk-table__header URN - %th.govuk-table__header Organisation name - %th.govuk-table__header Sector - %th.govuk-table__header Postcode - %th.govuk-table__header Published - %tbody.govuk-table__body - - @customers.each do |customer| - %tr.govuk-table__row - %td.govuk-table__cell= customer.urn - %td.govuk-table__cell= customer.name - %td.govuk-table__cell= customer.sector.titleize - %td.govuk-table__cell= customer.postcode - %td.govuk-table__cell= customer.published? ? 'true' : 'false' - %nav.pagination.ccs-pagination{"aria-label" => "Pagination", :role => "navigation"} - .ccs-pagination__summary= page_entries_info @customers, entry_name: "customer" - = paginate @customers + %li.govuk-tabs__list-item + %a.govuk-tabs__tab{href: '#inactive-urns'} + Inactive URNs + + #active-urns.govuk-tabs__panel + .results{id: 'active-customers-table'}= render 'active_customers', customers: @customers + + + #inactive-urns.govuk-tabs__panel.govuk-tabs__panel + .results{id: 'inactive-customers-table'}= render 'inactive_customers', inactive_customers: @inactive_customers - - else params[:search] - %p - No customers found for ‘#{params[:search]}’. + \ No newline at end of file diff --git a/app/views/admin/urns/index.js.haml b/app/views/admin/urns/index.js.haml new file mode 100644 index 000000000..c5b08983c --- /dev/null +++ b/app/views/admin/urns/index.js.haml @@ -0,0 +1,8 @@ +-if params[:active_page] || params.key?(:active_search) + $('#active-customers-table').html("#{j (render partial: 'active_customers', locals: {customers: @customers})}") + $('#active-customers-pagination').html("#{j (paginate(@customers, :param_name => "active_page", :remote => true).to_s)}"); + $('#active-customers-pagination-summary').html("#{j (page_entries_info(@customers, entry_name: "customer").to_s)}"); +-if params[:inactive_page] || params.key?(:inactive_search) + $('#inactive-customers-table').html("#{j (render partial: 'inactive_customers', locals: {inactive_customers: @inactive_customers})}") + $('#inactive-customers-pagination').html("#{j (paginate(@inactive_customers, :param_name => "inactive_page", :remote => true).to_s)}"); + $('#inactive-customers-pagination-summary').html("#{j (page_entries_info(@inactive_customers, entry_name: "inactive customer").to_s)}"); diff --git a/spec/features/admin_can_search_urns_spec.rb b/spec/features/admin_can_search_urns_spec.rb index e0c578975..cbffa2f09 100644 --- a/spec/features/admin_can_search_urns_spec.rb +++ b/spec/features/admin_can_search_urns_spec.rb @@ -20,8 +20,8 @@ scenario 'Searching by customer name' do visit admin_urns_path - fill_in 'Search', with: 'One' - click_button 'Search' + fill_in 'active-search', with: 'One' + click_button 'active-search-button' expect(page).to have_content '123' expect(page).to_not have_content '456' expect(page).to_not have_content '789' @@ -29,8 +29,8 @@ scenario 'Searching by URN' do visit admin_urns_path - fill_in 'Search', with: '456' - click_button 'Search' + fill_in 'active-search', with: '456' + click_button 'active-search-button' expect(page).to_not have_content '123' expect(page).to have_content '456' expect(page).to_not have_content '789' @@ -38,8 +38,8 @@ scenario 'Searching by postcode' do visit admin_urns_path - fill_in 'Search', with: 'IJ5 6KL' - click_button 'Search' + fill_in 'active-search', with: 'IJ5 6KL' + click_button 'active-search-button' expect(page).to_not have_content '123' expect(page).to_not have_content '456' expect(page).to have_content '789' diff --git a/spec/models/inactive_customer_spec.rb b/spec/models/inactive_customer_spec.rb index 42d81ca30..23224bd64 100644 --- a/spec/models/inactive_customer_spec.rb +++ b/spec/models/inactive_customer_spec.rb @@ -5,4 +5,51 @@ it { is_expected.to validate_presence_of(:inactive_urn) } it { is_expected.to validate_uniqueness_of(:inactive_urn) } + + describe '.search' do + let!(:inactive_customer1) { FactoryBot.create(:inactive_customer, inactive_urn: 123, inactive_customer_name: 'Customer One', replacement_urn: 456, replacement_customer_name: 'Replacement One', replacement_post_code: 'AB12 3CD') } + let!(:inactive_customer2) { FactoryBot.create(:inactive_customer, inactive_urn: 789, inactive_customer_name: 'Customer Two', replacement_urn: 101, replacement_customer_name: 'Replacement Two', replacement_post_code: 'EF45 6GH') } + + context 'when query is blank' do + it 'returns all inactive customers' do + expect(InactiveCustomer.search('')).to match_array([inactive_customer1, inactive_customer2]) + end + end + + context 'when query matches inactive_urn' do + it 'returns the matching inactive customer' do + expect(InactiveCustomer.search('123')).to match_array([inactive_customer1]) + end + end + + context 'when query matches inactive_customer_name' do + it 'returns the matching inactive customer' do + expect(InactiveCustomer.search('Customer Two')).to match_array([inactive_customer2]) + end + end + + context 'when query matches replacement_urn' do + it 'returns the matching inactive customer' do + expect(InactiveCustomer.search('456')).to match_array([inactive_customer1]) + end + end + + context 'when query matches replacement_customer_name' do + it 'returns the matching inactive customer' do + expect(InactiveCustomer.search('Replacement Two')).to match_array([inactive_customer2]) + end + end + + context 'when query matches replacement_post_code' do + it 'returns the matching inactive customer' do + expect(InactiveCustomer.search('AB12 3CD')).to match_array([inactive_customer1]) + end + end + + context 'when query does not match any attributes' do + it 'returns an empty result set' do + expect(InactiveCustomer.search('Nonexistent')).to be_empty + end + end + end end diff --git a/spec/requests/admin/urns_spec.rb b/spec/requests/admin/urns_spec.rb index 41dcc1a75..339724109 100644 --- a/spec/requests/admin/urns_spec.rb +++ b/spec/requests/admin/urns_spec.rb @@ -11,6 +11,60 @@ end describe 'GET /admin/urns' do + let!(:active_customer) do + create(:customer, urn: '123', name: 'Active Customer One', postcode: 'AB1 2CD', sector: :central_government) + end + + let!(:inactive_customer) do + create(:inactive_customer, inactive_urn: '456', inactive_customer_name: 'Inactive Customer ltd', replacement_urn: '789', replacement_customer_name: 'Replacement Customer', replacement_post_code: 'EF4 5GH') + end + + it 'renders the active and inactive URN tabs' do + get admin_urns_path + + expect(response).to have_http_status(:ok) + expect(response.body).to include('Active URNs') + expect(response.body).to include('Inactive URNs') + expect(response.body).to include('Download Active URN list') + end + + it 'shows active customers' do + get admin_urns_path + + expect(response.body).to include(active_customer.name) + expect(response.body).to include(active_customer.urn.to_s) + expect(response.body).to include(active_customer.postcode) + end + + it 'shows inactive customers' do + get admin_urns_path + + expect(response.body).to include(inactive_customer.inactive_customer_name) + expect(response.body).to include(inactive_customer.inactive_urn.to_s) + expect(response.body).to include(inactive_customer.replacement_customer_name) + expect(response.body).to include(inactive_customer.replacement_urn.to_s) + end + + it 'filters active customers independently' do + other_active_customer = create(:customer, urn: '999', name: 'Other Active Customer', postcode: 'XY1 2ZQ') + + get admin_urns_path, params: { active_search: 'Other Active Customer' } + + expect(response.body).to include(other_active_customer.name) + expect(response.body).not_to include(active_customer.name) + expect(response.body).to include(inactive_customer.inactive_customer_name) + end + + it 'filters inactive customers independently' do + other_inactive_customer = create(:inactive_customer, inactive_urn: '888', inactive_customer_name: 'Other Inactive Customer', replacement_urn: '777', replacement_customer_name: 'Other Replacement Customer', replacement_post_code: 'GH1 2IJ') + + get admin_urns_path, params: { inactive_search: 'Other Inactive Customer' } + + expect(response.body).to include(other_inactive_customer.inactive_customer_name) + expect(response.body).not_to include(inactive_customer.inactive_customer_name) + expect(response.body).to include(active_customer.name) + end + it 'renders the URN search page' do get admin_urns_path From 9912a78051e4b8cb628765ac9ced88b730f4e696 Mon Sep 17 00:00:00 2001 From: Reem Ibrahim Date: Thu, 20 Aug 2026 09:35:37 +0100 Subject: [PATCH 4/5] rubocop --- app/controllers/admin/urns_controller.rb | 10 ++++++---- app/models/inactive_customer.rb | 2 +- spec/models/inactive_customer_spec.rb | 10 ++++++++-- spec/requests/admin/urns_spec.rb | 7 +++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/controllers/admin/urns_controller.rb b/app/controllers/admin/urns_controller.rb index caa4a8541..7f137d364 100644 --- a/app/controllers/admin/urns_controller.rb +++ b/app/controllers/admin/urns_controller.rb @@ -1,15 +1,16 @@ require 'csv' class Admin::UrnsController < AdminController + # rubocop:disable Metrics/AbcSize def index @active_search = params[:active_search].to_s.strip @inactive_search = params[:inactive_search].to_s.strip @customers = Customer - .where(deleted: false) - .search(@active_search) - .order(:name) - .page(params[:active_page]) + .where(deleted: false) + .search(@active_search) + .order(:name) + .page(params[:active_page]) @inactive_customers = InactiveCustomer .search(@inactive_search) .order(date_made_inactive: :desc) @@ -20,6 +21,7 @@ def index format.js end end + # rubocop:enable Metrics/AbcSize def download send_data urn_csv, diff --git a/app/models/inactive_customer.rb b/app/models/inactive_customer.rb index 5690cfb9f..773aa7f08 100644 --- a/app/models/inactive_customer.rb +++ b/app/models/inactive_customer.rb @@ -6,7 +6,7 @@ def self.search(query) all else where( - 'cast(inactive_urn as text) ILIKE :query + 'cast(inactive_urn as text) ILIKE :query OR inactive_customer_name ILIKE :query OR cast(replacement_urn as text) ILIKE :query OR replacement_customer_name ILIKE :query diff --git a/spec/models/inactive_customer_spec.rb b/spec/models/inactive_customer_spec.rb index 23224bd64..552760e9f 100644 --- a/spec/models/inactive_customer_spec.rb +++ b/spec/models/inactive_customer_spec.rb @@ -7,8 +7,14 @@ it { is_expected.to validate_uniqueness_of(:inactive_urn) } describe '.search' do - let!(:inactive_customer1) { FactoryBot.create(:inactive_customer, inactive_urn: 123, inactive_customer_name: 'Customer One', replacement_urn: 456, replacement_customer_name: 'Replacement One', replacement_post_code: 'AB12 3CD') } - let!(:inactive_customer2) { FactoryBot.create(:inactive_customer, inactive_urn: 789, inactive_customer_name: 'Customer Two', replacement_urn: 101, replacement_customer_name: 'Replacement Two', replacement_post_code: 'EF45 6GH') } + let!(:inactive_customer1) do + FactoryBot.create(:inactive_customer, inactive_urn: 123, inactive_customer_name: 'Customer One', + replacement_urn: 456, replacement_customer_name: 'Replacement One', replacement_post_code: 'AB12 3CD') + end + let!(:inactive_customer2) do + FactoryBot.create(:inactive_customer, inactive_urn: 789, inactive_customer_name: 'Customer Two', + replacement_urn: 101, replacement_customer_name: 'Replacement Two', replacement_post_code: 'EF45 6GH') + end context 'when query is blank' do it 'returns all inactive customers' do diff --git a/spec/requests/admin/urns_spec.rb b/spec/requests/admin/urns_spec.rb index 339724109..463a9502f 100644 --- a/spec/requests/admin/urns_spec.rb +++ b/spec/requests/admin/urns_spec.rb @@ -16,7 +16,8 @@ end let!(:inactive_customer) do - create(:inactive_customer, inactive_urn: '456', inactive_customer_name: 'Inactive Customer ltd', replacement_urn: '789', replacement_customer_name: 'Replacement Customer', replacement_post_code: 'EF4 5GH') + create(:inactive_customer, inactive_urn: '456', inactive_customer_name: 'Inactive Customer ltd', +replacement_urn: '789', replacement_customer_name: 'Replacement Customer', replacement_post_code: 'EF4 5GH') end it 'renders the active and inactive URN tabs' do @@ -56,7 +57,9 @@ end it 'filters inactive customers independently' do - other_inactive_customer = create(:inactive_customer, inactive_urn: '888', inactive_customer_name: 'Other Inactive Customer', replacement_urn: '777', replacement_customer_name: 'Other Replacement Customer', replacement_post_code: 'GH1 2IJ') + other_inactive_customer = create(:inactive_customer, inactive_urn: '888', +inactive_customer_name: 'Other Inactive Customer', replacement_urn: '777', +replacement_customer_name: 'Other Replacement Customer', replacement_post_code: 'GH1 2IJ') get admin_urns_path, params: { inactive_search: 'Other Inactive Customer' } From 8693dc9b2667ce283c531240d503cf77c160dc2c Mon Sep 17 00:00:00 2001 From: Reem Ibrahim Date: Fri, 21 Aug 2026 14:33:36 +0100 Subject: [PATCH 5/5] amended replacement urn to be blank if zero --- app/views/admin/urns/_inactive_customers.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/urns/_inactive_customers.html.haml b/app/views/admin/urns/_inactive_customers.html.haml index a5118d3e9..c75c2b470 100644 --- a/app/views/admin/urns/_inactive_customers.html.haml +++ b/app/views/admin/urns/_inactive_customers.html.haml @@ -38,7 +38,7 @@ = inactive_customer.replacement_customer_name %br/ %small - = inactive_customer.replacement_urn + = inactive_customer.replacement_urn unless inactive_customer.replacement_urn.zero? %td.govuk-table__cell= inactive_customer.replacement_post_code %td.govuk-table__cell= inactive_customer.replacement_status %nav.pagination.ccs-pagination{"aria-label" => "Inactive URN pagination", :role => "navigation"}