diff --git a/app/controllers/admin/urn_lists_controller.rb b/app/controllers/admin/urn_lists_controller.rb index d6eefbb0b..8736213d9 100644 --- a/app/controllers/admin/urn_lists_controller.rb +++ b/app/controllers/admin/urn_lists_controller.rb @@ -1,8 +1,12 @@ class Admin::UrnListsController < AdminController - before_action :find_latest_list, only: %i[index] - def index - @urn_lists = UrnList.order(created_at: :desc).page(params[:page]) + @urn_lists = UrnList.order(created_at: :desc).page(params[:active_page]).per(25) + @inactive_customer_imports = InactiveCustomerImport.order(created_at: :desc).page(params[:inactive_page]).per(25) + + respond_to do |format| + format.html + format.js + end end def new @@ -27,10 +31,6 @@ def urn_list_params params.require(:urn_list).permit(:excel_file) end - def find_latest_list - @latest_urn_list = UrnList.where(source: 'manual_upload', aasm_state: 'processed').order(created_at: :desc).first - end - def s3_client @s3_client ||= Aws::S3::Client.new(region: ENV['AWS_S3_REGION']) end diff --git a/app/jobs/inactive_urn_list_api_sync_job.rb b/app/jobs/inactive_urn_list_api_sync_job.rb index efbdc85c0..50dda7a97 100644 --- a/app/jobs/inactive_urn_list_api_sync_job.rb +++ b/app/jobs/inactive_urn_list_api_sync_job.rb @@ -1,7 +1,22 @@ class InactiveUrnListApiSyncJob < ApplicationJob def perform + inactive_customer_import = InactiveCustomerImport.create!(aasm_state: :pending) + rows = UrnLists::ApiClient.new.fetch_inactive_rows + count = UrnLists::ImportInactiveCustomers.new(rows: rows).call + + inactive_customer_import.update!( + aasm_state: :processed, + records_count: count, + completed_at: Time.current + ) + rescue StandardError => e + inactive_customer_import&.update!( + aasm_state: :failed, + completed_at: Time.current, + records_count: count || 0 + ) - UrnLists::ImportInactiveCustomers.new(rows: rows).call + raise e end end diff --git a/app/models/inactive_customer_import.rb b/app/models/inactive_customer_import.rb new file mode 100644 index 000000000..339d3029e --- /dev/null +++ b/app/models/inactive_customer_import.rb @@ -0,0 +1,9 @@ +class InactiveCustomerImport < ApplicationRecord + include AASM + + aasm do + state :pending, initial: true + state :processed + state :failed + end +end diff --git a/app/services/urn_lists/import_inactive_customers.rb b/app/services/urn_lists/import_inactive_customers.rb index 9e2db989c..984c255b8 100644 --- a/app/services/urn_lists/import_inactive_customers.rb +++ b/app/services/urn_lists/import_inactive_customers.rb @@ -14,6 +14,8 @@ def call unique_by: :index_inactive_customers_on_inactive_urn ) end + + rows.count end # rubocop:enable Rails/SkipsModelValidations diff --git a/app/views/admin/urn_lists/_active_urn_lists.html.haml b/app/views/admin/urn_lists/_active_urn_lists.html.haml new file mode 100644 index 000000000..76c1d5106 --- /dev/null +++ b/app/views/admin/urn_lists/_active_urn_lists.html.haml @@ -0,0 +1,17 @@ +%table.govuk-table{:class => 'govuk-!-margin-top-7'} + %thead.govuk-table__head + %tr.govuk-table__row + %th.govuk-table__header Source + %th.govuk-table__header Filename + %th.govuk-table__header Upload Date + %th.govuk-table__header Status + %tbody.govuk-table__body + - @urn_lists.each do |list| + %tr.govuk-table__row + %td.govuk-table__cell= list.source.humanize + %td.govuk-table__cell= list.excel_file.filename || '-' + %td.govuk-table__cell= list.created_at + %td.govuk-table__cell= list.aasm_state +%nav.pagination.ccs-pagination{"aria-label" => "Pagination", :role => "navigation"} + #active_audit_log_pagination_summary.ccs-pagination__summary= page_entries_info @urn_lists, entry_name: "URN list" + #active_audit_log_pagination= paginate @urn_lists, :param_name => "active_page", remote: true \ No newline at end of file diff --git a/app/views/admin/urn_lists/_inactive_customer_imports.html.haml b/app/views/admin/urn_lists/_inactive_customer_imports.html.haml new file mode 100644 index 000000000..0db8bf92b --- /dev/null +++ b/app/views/admin/urn_lists/_inactive_customer_imports.html.haml @@ -0,0 +1,13 @@ +%table.govuk-table{:class => 'govuk-!-margin-top-7'} + %thead.govuk-table__head + %tr.govuk-table__row + %th.govuk-table__header Sync Datetime + %th.govuk-table__header Status + %tbody.govuk-table__body + - @inactive_customer_imports.each do |import| + %tr.govuk-table__row + %td.govuk-table__cell= import.created_at + %td.govuk-table__cell= import.aasm_state +%nav.pagination.ccs-pagination{"aria-label" => "Pagination", :role => "navigation"} + #inactive_audit_log_pagination_summary.ccs-pagination__summary= page_entries_info @inactive_customer_imports, entry_name: "import" + #inactive_audit_log_pagination= paginate @inactive_customer_imports, :param_name => "inactive_page", remote: true \ No newline at end of file diff --git a/app/views/admin/urn_lists/index.html.haml b/app/views/admin/urn_lists/index.html.haml index 8dcfb2afc..ee3a59988 100644 --- a/app/views/admin/urn_lists/index.html.haml +++ b/app/views/admin/urn_lists/index.html.haml @@ -8,27 +8,25 @@ %h2#page-actions-title.govuk-heading-s{"aria-label" => "Page actions"} Actions %ul.govuk-page-actions--actions %li.govuk-page-actions--action - = link_to 'Add a new URN list', new_admin_urn_list_path + = 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 .govuk-grid-row .govuk-grid-column-full - - if @urn_lists.present? - %table.govuk-table{:class => 'govuk-!-margin-top-7'} - %thead.govuk-table__head - %tr.govuk-table__row - %th.govuk-table__header Source - %th.govuk-table__header Filename - %th.govuk-table__header Upload Date - %th.govuk-table__header Status - %tbody.govuk-table__body - - @urn_lists.each do |list| - %tr.govuk-table__row - %td.govuk-table__cell= list.source.humanize - %td.govuk-table__cell= list.excel_file.filename || '-' - %td.govuk-table__cell= list.created_at - %td.govuk-table__cell= list.aasm_state - = paginate @urn_lists - - + .govuk-tabs{"data-module" => "govuk-tabs"} + %h2.govuk-tabs__title + Contents + %ul.govuk-tabs__list + %li.govuk-tabs__list-item.govuk-tabs__list-item--selected + %a.govuk-tabs__tab{href: "#active"} + Active + %li.govuk-tabs__list-item + %a.govuk-tabs__tab{href: "#inactive"} + Inactive + #active.govuk-tabs__panel + - if @urn_lists.present? + .results{id: 'active-urn-lists-table'}= render 'active_urn_lists', urn_lists: @urn_lists + #inactive.govuk-tabs__panel + - if @inactive_customer_imports.present? + .results{id: 'inactive-customer-imports-table'}= render 'inactive_customer_imports', imports: @inactive_customer_imports diff --git a/app/views/admin/urn_lists/index.js.haml b/app/views/admin/urn_lists/index.js.haml new file mode 100644 index 000000000..f59cfc462 --- /dev/null +++ b/app/views/admin/urn_lists/index.js.haml @@ -0,0 +1,8 @@ +-if params[:active_page] + $('#active-urn-lists-table').html("#{j (render partial: 'active_urn_lists', locals: {urn_lists: @urn_lists})}") + $('#active_audit_log_pagination').html("#{j (paginate(@urn_lists, :param_name => "active_page", :remote => true).to_s)}"); + $('#active_audit_log_pagination_summary').html("#{j (page_entries_info(@urn_lists, entry_name: "URN list").to_s)}"); +-if params[:inactive_page] + $('#inactive-customer-imports-table').html("#{j (render partial: 'inactive_customer_imports', locals: {imports: @inactive_customer_imports})}") + $('#inactive_audit_log_pagination').html("#{j (paginate(@inactive_customer_imports, :param_name => "inactive_page", :remote => true).to_s)}"); + $('#inactive_audit_log_pagination_summary').html("#{j (page_entries_info(@inactive_customer_imports, entry_name: "import").to_s)}"); diff --git a/config/sidekiq_schedule.yml b/config/sidekiq_schedule.yml index 518c94690..18b4951a5 100644 --- a/config/sidekiq_schedule.yml +++ b/config/sidekiq_schedule.yml @@ -18,3 +18,4 @@ inactive_urn_list_importer: cron: '0 20 * * * Europe/London' class: InactiveUrnListApiSyncJob queue: default + active_job: true diff --git a/db/migrate/20260605131714_create_inactive_customer_imports.rb b/db/migrate/20260605131714_create_inactive_customer_imports.rb new file mode 100644 index 000000000..b2bbf8cf2 --- /dev/null +++ b/db/migrate/20260605131714_create_inactive_customer_imports.rb @@ -0,0 +1,11 @@ +class CreateInactiveCustomerImports < ActiveRecord::Migration[8.1] + def change + create_table :inactive_customer_imports, id: :uuid do |t| + t.string :aasm_state + t.integer :records_count + t.datetime :completed_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 807ed2d1a..437a98274 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_05_18_115711) do +ActiveRecord::Schema[8.1].define(version: 2026_06_05_131714) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_catalog.plpgsql" @@ -162,6 +162,14 @@ t.index ["short_name"], name: "index_frameworks_on_short_name", unique: true end + create_table "inactive_customer_imports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "aasm_state" + t.datetime "completed_at" + t.datetime "created_at", null: false + t.integer "records_count" + t.datetime "updated_at", null: false + end + create_table "inactive_customers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.date "date_made_inactive" diff --git a/spec/factories/inactive_customer_imports.rb b/spec/factories/inactive_customer_imports.rb new file mode 100644 index 000000000..23b0cfeae --- /dev/null +++ b/spec/factories/inactive_customer_imports.rb @@ -0,0 +1,19 @@ +FactoryBot.define do + factory :inactive_customer_import do + aasm_state { :pending } + records_count { 0 } + completed_at { nil } + + trait :processed do + aasm_state { :processed } + records_count { 100 } + completed_at { Time.current } + end + + trait :failed do + aasm_state { :failed } + records_count { 0 } + completed_at { Time.current } + end + end +end diff --git a/spec/features/admin_can_upload_urn_list_spec.rb b/spec/features/admin_can_upload_urn_list_spec.rb index c620374fb..9763c9a78 100644 --- a/spec/features/admin_can_upload_urn_list_spec.rb +++ b/spec/features/admin_can_upload_urn_list_spec.rb @@ -12,7 +12,7 @@ scenario 'uploading a URN list' do visit admin_urn_lists_path - click_link 'Add a new URN list' + click_link 'Add a new Active URN list' expect(page).to have_text 'Upload a new URN list' diff --git a/spec/jobs/inactive_urn_list_api_sync_job.rb b/spec/jobs/inactive_urn_list_api_sync_job.rb deleted file mode 100644 index 00ec2c23a..000000000 --- a/spec/jobs/inactive_urn_list_api_sync_job.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'rails_helper' - -RSpec.describe InactiveUrnListApiSyncJob do - describe '#perform' do - let(:client) { instance_double(UrnLists::ApiClient) } - - let(:rows) do - [ - { - 'InactiveURN' => '10009655', - 'InactiveCustomerName' => 'Government Commercial Agency', - 'DateMadeInactive' => '2024-01-01', - 'ReplacementURN' => '10009656', - 'ReplacementName' => 'Another Organisation', - 'ReplacementPostCode' => 'AB1 2CD', - 'ReplacementStatus' => 'active' - } - ] - end - - before do - allow(UrnLists::ApiClient).to receive(:new).and_return(client) - allow(client).to receive(:fetch_inactive_customers).and_return(rows) - end - - it 'fetches inactive customers and imports them into the database' do - expect do - described_class.perform_now - end.to change(InactiveCustomer, :count).by(1) - end - end -end diff --git a/spec/jobs/inactive_urn_list_api_sync_job_spec.rb b/spec/jobs/inactive_urn_list_api_sync_job_spec.rb new file mode 100644 index 000000000..090d7f7e4 --- /dev/null +++ b/spec/jobs/inactive_urn_list_api_sync_job_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +RSpec.describe InactiveUrnListApiSyncJob do + describe '#perform' do + let(:rows) do + [ + { + 'InactiveURN' => '10009655', + 'InactiveCustomerName' => 'Government Commercial Agency', + 'DateMadeInactive' => '2024-01-01', + 'ReplacementURN' => '10009656', + 'ReplacementName' => 'Another Organisation', + 'ReplacementPostCode' => 'AB1 2CD', + 'ReplacementStatus' => 'active' + } + ] + end + + let(:api_client_service) do + double('UrnLists::ApiClient', fetch_inactive_rows: rows) + end + + let(:import_inactive_customers_service) do + double('UrnLists::ImportInactiveCustomers', call: rows.count) + end + + before do + allow(UrnLists::ApiClient).to receive(:new).and_return(api_client_service) + allow(UrnLists::ImportInactiveCustomers) + .to receive(:new).with(rows: rows) + .and_return(import_inactive_customers_service) + end + + it 'creates a pending inactive customer import, imports the rows, and marks it as processed' do + expect do + described_class.perform_now + end.to change(InactiveCustomerImport, :count).by(1) + + expect(api_client_service).to have_received(:fetch_inactive_rows) + expect(import_inactive_customers_service).to have_received(:call) + + inactive_customer_import = InactiveCustomerImport.last + + expect(inactive_customer_import.aasm_state).to eq('processed') + expect(inactive_customer_import.records_count).to eq(rows.count) + expect(inactive_customer_import.completed_at).not_to be_nil + end + + it 'handles errors during the import process and marks the import as failed' do + allow(api_client_service).to receive(:fetch_inactive_rows).and_raise(StandardError.new('API error')) + + expect do + described_class.perform_now + end.to raise_error(StandardError, 'API error') + + inactive_customer_import = InactiveCustomerImport.last + + expect(inactive_customer_import.aasm_state).to eq('failed') + expect(inactive_customer_import.records_count).to eq(0) + expect(inactive_customer_import.completed_at).not_to be_nil + end + + it 'marks the import as failed when the import fails after rows are fetched' do + allow(import_inactive_customers_service).to receive(:call).and_raise(StandardError.new('Import error')) + + expect do + described_class.perform_now + end.to raise_error(StandardError, 'Import error') + + inactive_customer_import = InactiveCustomerImport.last + + expect(inactive_customer_import.aasm_state).to eq('failed') + expect(inactive_customer_import.records_count).to eq(0) + expect(inactive_customer_import.completed_at).not_to be_nil + end + end +end diff --git a/spec/services/urn_lists/import_inactive_customers_spec.rb b/spec/services/urn_lists/import_inactive_customers_spec.rb index 9bc49ad68..a56e76ae2 100644 --- a/spec/services/urn_lists/import_inactive_customers_spec.rb +++ b/spec/services/urn_lists/import_inactive_customers_spec.rb @@ -17,10 +17,14 @@ end it 'imports inactive customers into the database' do + result = nil + expect do - described_class.new(rows: rows).call + result = described_class.new(rows: rows).call end.to change(InactiveCustomer, :count).by(1) + expect(result).to eq(rows.count) + inactive_customer = InactiveCustomer.last expect(inactive_customer.inactive_urn).to eq(10009655) expect(inactive_customer.inactive_customer_name).to eq('Government Commercial Agency') @@ -45,6 +49,8 @@ expect do described_class.new(rows: rows).call end.not_to change(InactiveCustomer, :count) + + expect(described_class.new(rows: rows).call).to eq(rows.count) end end end