diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb new file mode 100644 index 000000000..a42e39944 --- /dev/null +++ b/app/controllers/admin/reports_controller.rb @@ -0,0 +1,18 @@ +module Admin + class ReportsController < BaseAdminController + before_action :ensure_service_operator + + def index + @reports = Report.order(created_at: :desc) + end + + def show + respond_to do |format| + format.csv { + report = Report.find(params[:id]) + send_data report.csv, filename: "#{report.name.parameterize(separator: "_")}_#{report.created_at.iso8601}.csv" + } + end + end + end +end diff --git a/app/jobs/reports_job.rb b/app/jobs/reports_job.rb new file mode 100644 index 000000000..8032c8408 --- /dev/null +++ b/app/jobs/reports_job.rb @@ -0,0 +1,14 @@ +class ReportsJob < CronJob + self.cron_expression = "0 6 * * 2#2" # second Tuesday of the month + + def perform + Rails.logger.info "Generating Ops reports" + + csv = Reports::DuplicateClaims.new.to_csv + Report.create!(name: Reports::DuplicateClaims::NAME, csv: csv, number_of_rows: csv.lines.count - 1) + csv = Reports::FailedQualificationClaims.new.to_csv + Report.create!(name: Reports::FailedQualificationClaims::NAME, csv: csv, number_of_rows: csv.lines.count - 1) + csv = Reports::FailedProviderCheckClaims.new.to_csv + Report.create!(name: Reports::FailedProviderCheckClaims::NAME, csv: csv, number_of_rows: csv.lines.count - 1) + end +end diff --git a/app/models/report.rb b/app/models/report.rb new file mode 100644 index 000000000..1dfff22ff --- /dev/null +++ b/app/models/report.rb @@ -0,0 +1,2 @@ +class Report < ApplicationRecord +end diff --git a/app/models/reports/duplicate_claims.rb b/app/models/reports/duplicate_claims.rb new file mode 100644 index 000000000..1d3610ad5 --- /dev/null +++ b/app/models/reports/duplicate_claims.rb @@ -0,0 +1,47 @@ +require "csv" +require "excel_utils" + +module Reports + class DuplicateClaims + include Admin::ClaimsHelper + + NAME = "Duplicate Approved Claims" + HEADERS = [ + "Claim reference", + "Teacher reference number", + "Full name", + "Policy name", + "Claim amount", + "Claim status", + "Decision date", + "Decision agent" + ].freeze + + def initialize + @claims = Claim.approved.select { |claim| Claim::MatchingAttributeFinder.new(claim).matching_claims.any? } + end + + def to_csv + CSV.generate(write_headers: true, headers: HEADERS) do |csv| + @claims.each do |claim| + csv << row( + claim.reference, + claim.eligibility.teacher_reference_number, + claim.full_name, + claim.policy, + claim.award_amount, + status(claim), + claim.latest_decision.created_at, + claim.latest_decision.created_by.full_name + ) + end + end + end + + private + + def row(*entries) + entries.map { |entry| ExcelUtils.escape_formulas(entry) } + end + end +end diff --git a/app/models/reports/failed_provider_check_claims.rb b/app/models/reports/failed_provider_check_claims.rb new file mode 100644 index 000000000..d3782a148 --- /dev/null +++ b/app/models/reports/failed_provider_check_claims.rb @@ -0,0 +1,72 @@ +require "csv" +require "excel_utils" + +module Reports + class FailedProviderCheckClaims + include Admin::ClaimsHelper + + def self.provider_verification_label(field) + I18n.t("further_education_payments.admin.task_questions.provider_verification.#{field}.label") + end + private_class_method :provider_verification_label + + NAME = "Claims with failed provider check" + HEADERS = [ + "Claim reference", + "Teacher reference number", + "Full name", + "Claim amount", + "Claim status", + "Decision date", + "Decision agent", + "Provider response: #{provider_verification_label("contract_type")}", + "Provider response: #{provider_verification_label("teaching_responsibilities")}", + "Provider response: #{provider_verification_label("further_education_teaching_start_year")}", + "Provider response: #{provider_verification_label("teaching_hours_per_week")}", + "Provider response: #{provider_verification_label("half_teaching_hours")}", + "Provider response: #{provider_verification_label("subjects_taught")}", + "Provider response: #{provider_verification_label("taught_at_least_one_term")}", + "Provider response: #{provider_verification_label("teaching_hours_per_week_next_term")}" + ].freeze + + def initialize + @claims = Claim.includes(:tasks) + .where(eligibility_type: "Policies::FurtherEducationPayments::Eligibility", tasks: {name: "provider_verification", passed: false}) + .approved + end + + def to_csv + CSV.generate(write_headers: true, headers: HEADERS) do |csv| + @claims.each do |claim| + csv << row( + claim.reference, + claim.eligibility.teacher_reference_number, + claim.full_name, + claim.award_amount, + status(claim), + claim.latest_decision.created_at, + claim.latest_decision.created_by.full_name, + verification_assertion(claim, "contract_type"), + verification_assertion(claim, "teaching_responsibilities"), + verification_assertion(claim, "further_education_teaching_start_year"), + verification_assertion(claim, "teaching_hours_per_week"), + verification_assertion(claim, "half_teaching_hours"), + verification_assertion(claim, "subjects_taught"), + verification_assertion(claim, "taught_at_least_one_term"), + verification_assertion(claim, "teaching_hours_per_week_next_term") + ) + end + end + end + + private + + def row(*entries) + entries.map { |entry| ExcelUtils.escape_formulas(entry) } + end + + def verification_assertion(claim, name) + claim.eligibility["assertions"].find { |assertion| assertion["name"] == name }["outcome"] + end + end +end diff --git a/app/models/reports/failed_qualification_claims.rb b/app/models/reports/failed_qualification_claims.rb new file mode 100644 index 000000000..dc73e7f41 --- /dev/null +++ b/app/models/reports/failed_qualification_claims.rb @@ -0,0 +1,77 @@ +require "csv" +require "excel_utils" + +module Reports + class FailedQualificationClaims + NAME = "Claims with failed qualification status" + HEADERS = [ + "Claim reference", + "Teacher reference number", + "Policy name", + "Decision date", + "Decision agent", + "Answered qualification", + "Answered ITT start year", + "Answered ITT subject", + "DQT ITT subjects", + "DQT ITT start year", + "DQT QTS award date", + "DQT qualification name" + ].freeze + + def initialize + @claims = Claim.includes(:tasks).where(tasks: {name: "qualifications", passed: false}).approved + end + + def to_csv + CSV.generate(write_headers: true, headers: HEADERS) do |csv| + @claims.each do |claim| + csv << row( + claim.reference, + claim.eligibility.teacher_reference_number, + claim.policy, + claim.latest_decision.created_at, + claim.latest_decision.created_by.full_name, + claim.eligibility.qualification, + claim.eligibility.itt_academic_year.start_year, + claim.eligibility.eligible_itt_subject, + dqt_itt_subjects(claim), + dqt_itt_start_date(claim), + dqt_qts_date(claim), + dqt_qts_qualification_name(claim) + ) + end + end + end + + private + + def row(*entries) + entries.map { |entry| ExcelUtils.escape_formulas(entry) } + end + + def dqt_itt_subjects(claim) + unless claim.dqt_teacher_status.empty? + claim.dqt_teacher_status["initial_teacher_training"].fetch_values("subject1", "subject2", "subject3").compact.join(",") + end + end + + def dqt_itt_start_date(claim) + unless claim.dqt_teacher_status.empty? + claim.dqt_teacher_status["initial_teacher_training"]["programme_start_date"] + end + end + + def dqt_qts_date(claim) + unless claim.dqt_teacher_status.empty? + claim.dqt_teacher_status["qualified_teacher_status"]["qts_date"] + end + end + + def dqt_qts_qualification_name(claim) + unless claim.dqt_teacher_status.empty? + claim.dqt_teacher_status["qualified_teacher_status"]["name"] + end + end + end +end diff --git a/app/views/admin/reports/index.html.erb b/app/views/admin/reports/index.html.erb new file mode 100644 index 000000000..bf9ddf816 --- /dev/null +++ b/app/views/admin/reports/index.html.erb @@ -0,0 +1,32 @@ +<% content_for(:page_title) { page_title("Download Reports") } %> + +<% content_for :back_link do %> + <%= govuk_back_link href: admin_root_path %> +<% end %> + +
+
+

+ Reports +

+ + + + + + + + + + + <% @reports.each do |report| %> + + + + + + <% end %> + +
NameCreated AtNumber of rows
<%= govuk_link_to report.name, admin_report_path(report, format: :csv) %><%= l(report.created_at) %><%= report.number_of_rows %>
+
+
diff --git a/app/views/application/_admin_menu.html.erb b/app/views/application/_admin_menu.html.erb index 5e70776cf..a84eb0ee7 100644 --- a/app/views/application/_admin_menu.html.erb +++ b/app/views/application/_admin_menu.html.erb @@ -14,6 +14,9 @@
  • <%= link_to "Manage services", admin_journey_configurations_path, class: "govuk-header__link" %>
  • +
  • + <%= link_to "Reports", admin_reports_path, class: "govuk-header__link" %> +
  • <% end %>
  • <%= link_to "Sign out", admin_sign_out_path, method: :delete, class: "govuk-header__link" %> diff --git a/config/analytics_blocklist.yml b/config/analytics_blocklist.yml index df4942ffd..6c3e449ab 100644 --- a/config/analytics_blocklist.yml +++ b/config/analytics_blocklist.yml @@ -131,3 +131,10 @@ - verification :journeys_sessions: - answers + :reports: + - id + - name + - csv + - created_at + - updated_at + - number_of_rows diff --git a/config/routes.rb b/config/routes.rb index b8b19f1d6..fcea29e2d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,6 +159,7 @@ def matches?(request) resources :levelling_up_premium_payments_awards, only: [:index, :create] resource :eligible_ey_providers, only: [:create, :show], path: "eligible-early-years-providers" resource :eligible_fe_providers, only: [:create, :show], path: "eligible-further-education-providers" + resources :reports, only: [:index, :show] get "refresh-session", to: "sessions#refresh", as: :refresh_session diff --git a/db/migrate/20241007141638_create_reports.rb b/db/migrate/20241007141638_create_reports.rb new file mode 100644 index 000000000..59fce2631 --- /dev/null +++ b/db/migrate/20241007141638_create_reports.rb @@ -0,0 +1,11 @@ +class CreateReports < ActiveRecord::Migration[7.0] + def change + create_table :reports, id: :uuid do |t| + t.string :name + t.text :csv + t.integer :number_of_rows + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2aa35382c..71dde8e05 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[7.0].define(version: 2024_09_24_113642) do +ActiveRecord::Schema[7.0].define(version: 2024_10_07_141638) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_trgm" @@ -111,7 +111,7 @@ t.text "onelogin_idv_first_name" t.text "onelogin_idv_last_name" t.date "onelogin_idv_date_of_birth" - t.datetime "started_at", precision: nil, null: false + t.datetime "started_at", precision: nil t.index ["academic_year"], name: "index_claims_on_academic_year" t.index ["created_at"], name: "index_claims_on_created_at" t.index ["eligibility_type", "eligibility_id"], name: "index_claims_on_eligibility_type_and_eligibility_id" @@ -419,6 +419,14 @@ t.string "itt_subject" end + create_table "reports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name" + t.text "csv" + t.integer "number_of_rows" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "school_workforce_censuses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "teacher_reference_number" t.datetime "created_at", null: false