diff --git a/app/controllers/claims/support/claims/payment_responses_controller.rb b/app/controllers/claims/support/claims/payment_responses_controller.rb new file mode 100644 index 000000000..89a02c80c --- /dev/null +++ b/app/controllers/claims/support/claims/payment_responses_controller.rb @@ -0,0 +1,35 @@ +class Claims::Support::Claims::PaymentResponsesController < Claims::Support::ApplicationController + append_pundit_namespace :claims + + before_action :authorize_claims_payments_response + + helper_method :claims_payment_response + + def new + render "new_not_permitted" unless policy(claims_payment_response).update? + end + + def check + render "new" unless claims_payment_response.save + end + + def update + Claims::PaymentResponse::Process.call(payment_response: claims_payment_response, current_user:) + + redirect_to claims_support_claims_payments_path, flash: { success: true, heading: t(".success") } + end + + private + + def authorize_claims_payments_response + authorize claims_payment_response + end + + def claims_payment_response_params + params.fetch(:claims_payment_response, {}).permit(:csv_file).merge(user: current_user) + end + + def claims_payment_response + @claims_payment_response ||= params[:id] ? Claims::PaymentResponse.find(params[:id]) : Claims::PaymentResponse.new(claims_payment_response_params) + end +end diff --git a/app/jobs/claims/remove_unprocessed_payment_responses_job.rb b/app/jobs/claims/remove_unprocessed_payment_responses_job.rb new file mode 100644 index 000000000..398f4ba35 --- /dev/null +++ b/app/jobs/claims/remove_unprocessed_payment_responses_job.rb @@ -0,0 +1,7 @@ +class Claims::RemoveUnprocessedPaymentResponsesJob < ApplicationJob + queue_as :default + + def perform + Claims::PaymentResponse.where(processed: false, created_at: ..1.day.ago).find_each(&:destroy!) + end +end diff --git a/app/models/claims/claim.rb b/app/models/claims/claim.rb index afb2630c3..97e0cd97b 100644 --- a/app/models/claims/claim.rb +++ b/app/models/claims/claim.rb @@ -11,6 +11,7 @@ # status :enum # submitted_at :datetime # submitted_by_type :string +# unpaid_reason :text # created_at :datetime not null # updated_at :datetime not null # claim_window_id :uuid diff --git a/app/models/claims/payment_response.rb b/app/models/claims/payment_response.rb new file mode 100644 index 000000000..302d1e50a --- /dev/null +++ b/app/models/claims/payment_response.rb @@ -0,0 +1,29 @@ +# == Schema Information +# +# Table name: payment_responses +# +# id :uuid not null, primary key +# processed :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# user_id :uuid not null +# +# Indexes +# +# index_payment_responses_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +class Claims::PaymentResponse < ApplicationRecord + belongs_to :user + + has_one_attached :csv_file + + validates :csv_file, presence: true + + def row_count + @row_count ||= CSV.parse(csv_file.download, headers: true).length + end +end diff --git a/app/policies/claims/support/claims/payment_response_policy.rb b/app/policies/claims/support/claims/payment_response_policy.rb new file mode 100644 index 000000000..ab6f8bd8a --- /dev/null +++ b/app/policies/claims/support/claims/payment_response_policy.rb @@ -0,0 +1,9 @@ +class Claims::Support::Claims::PaymentResponsePolicy < Claims::ApplicationPolicy + def new? + true + end + + def update? + Claims::Claim.payment_in_progress.any? + end +end diff --git a/app/services/claims/payment_response/process.rb b/app/services/claims/payment_response/process.rb new file mode 100644 index 000000000..519969b27 --- /dev/null +++ b/app/services/claims/payment_response/process.rb @@ -0,0 +1,34 @@ +class Claims::PaymentResponse::Process < ApplicationService + def initialize(payment_response:, current_user:) + @payment_response = payment_response + @current_user = current_user + end + + def call + return if Claims::Claim.payment_in_progress.none? + + ActiveRecord::Base.transaction do + CSV.parse(payment_response.csv_file.download, headers: true).each do |row| + claim = Claims::Claim.find_by(reference: row["claim_reference"]) + + next unless claim + next unless claim.payment_in_progress? + + case row["claim_status"] + when "paid" + claim.update!(status: :paid) + when "unpaid" + claim.update!(status: :payment_information_requested, unpaid_reason: row["claim_unpaid_reason"]) + end + end + + Claims::ClaimActivity.create!(action: :payment_response_uploaded, user: current_user, record: payment_response) + + payment_response + end + end + + private + + attr_reader :payment_response, :current_user +end diff --git a/app/views/claims/support/claims/claim_activities/index.html.erb b/app/views/claims/support/claims/claim_activities/index.html.erb index 264d603e4..f045235c1 100644 --- a/app/views/claims/support/claims/claim_activities/index.html.erb +++ b/app/views/claims/support/claims/claim_activities/index.html.erb @@ -26,11 +26,13 @@
<% case claim_activity.action %> - <% when "payment_request_delivered", "clawback_request_delivered" %> + <% when "payment_request_delivered", "clawback_request_delivered", "payment_response_uploaded" %> diff --git a/app/views/claims/support/claims/payment_responses/check.html.erb b/app/views/claims/support/claims/payment_responses/check.html.erb new file mode 100644 index 000000000..09f070114 --- /dev/null +++ b/app/views/claims/support/claims/payment_responses/check.html.erb @@ -0,0 +1,23 @@ +<%= render "claims/support/primary_navigation", current: :claims %> +<% content_for(:page_title) { sanitize t(".page_title") } %> + +<% content_for(:before_content) do %> + <%= govuk_back_link href: claims_support_claims_payments_path %> +<% end %> + +
+
+
+ <%= form_with model: claims_payment_response, url: claims_support_claims_payment_response_path(claims_payment_response) do |f| %> +

<%= t(".page_caption") %>

+

<%= t(".page_title") %>

+ +

<%= t(".description", count: claims_payment_response.row_count) %>

+ + <%= f.govuk_submit t(".submit") %> + <% end %> + +

<%= govuk_link_to t(".cancel"), claims_support_claims_payments_path %>

+
+
+
diff --git a/app/views/claims/support/claims/payment_responses/new.html.erb b/app/views/claims/support/claims/payment_responses/new.html.erb new file mode 100644 index 000000000..245142c9c --- /dev/null +++ b/app/views/claims/support/claims/payment_responses/new.html.erb @@ -0,0 +1,36 @@ +<%= render "claims/support/primary_navigation", current: :claims %> +<% content_for(:page_title) { sanitize t(".page_title") } %> + +<% content_for(:before_content) do %> + <%= govuk_back_link href: claims_support_claims_payments_path %> +<% end %> + +
+
+
+ <%= form_with model: claims_payment_response, url: check_claims_support_claims_payment_responses_path do |f| %> + <%= f.govuk_error_summary %> + +

<%= t(".page_caption") %>

+

<%= t(".page_title") %>

+ + <%= f.govuk_file_field :csv_file, label: { text: t(".label") }, accept: "text/csv" %> + + <%= govuk_details summary_text: t(".help.label") do %> +

<%= t(".help.description") %>

+

<%= t(".help.csv_instructions") %>

+ + <%= govuk_list type: :bullet do %> + <% t(".help.csv_headers").each do |header| %> +
  • <%= header %>
  • + <% end %> + <% end %> + <% end %> + + <%= f.govuk_submit t(".submit") %> + <% end %> + +

    <%= govuk_link_to t(".cancel"), claims_support_claims_payments_path %>

    +
    +
    +
    diff --git a/app/views/claims/support/claims/payment_responses/new_not_permitted.html.erb b/app/views/claims/support/claims/payment_responses/new_not_permitted.html.erb new file mode 100644 index 000000000..4b7f5d5d0 --- /dev/null +++ b/app/views/claims/support/claims/payment_responses/new_not_permitted.html.erb @@ -0,0 +1,19 @@ +<%= render "claims/support/primary_navigation", current: :claims %> +<% content_for(:page_title) { sanitize t(".page_title") } %> + +<% content_for(:before_content) do %> + <%= govuk_back_link href: claims_support_claims_payments_path %> +<% end %> + +
    +
    +
    +

    <%= t(".page_caption") %>

    +

    <%= t(".page_title") %>

    + +

    <%= t(".description") %>

    + +

    <%= govuk_link_to t(".cancel"), claims_support_claims_payments_path %>

    +
    +
    +
    diff --git a/app/views/claims/support/claims/payments/claims/show.html.erb b/app/views/claims/support/claims/payments/claims/show.html.erb index d90cf3cac..058d305f2 100644 --- a/app/views/claims/support/claims/payments/claims/show.html.erb +++ b/app/views/claims/support/claims/payments/claims/show.html.erb @@ -11,6 +11,13 @@

    <%= t(".page_caption", reference: @claim.reference) %>

    <%= @claim.school_name %> <%= render Claim::StatusTagComponent.new(claim: @claim) %>

    + <% if @claim.unpaid_reason.present? %> + <%= govuk_inset_text do %> +

    <%= t(".unpaid_reason_header") %>

    +

    <%= @claim.unpaid_reason %>

    + <% end %> + <% end %> + <% if @claim.payment_information_requested? %>
    <%= govuk_button_link_to t(".buttons.information_sent"), confirm_information_sent_claims_support_claims_payments_claim_path(@claim) %> diff --git a/app/views/claims/support/claims/payments/index.html.erb b/app/views/claims/support/claims/payments/index.html.erb index d20fa4780..a1b3e17df 100644 --- a/app/views/claims/support/claims/payments/index.html.erb +++ b/app/views/claims/support/claims/payments/index.html.erb @@ -10,6 +10,7 @@
    <%= govuk_button_link_to t(".buttons.send_claims_to_esfa"), new_claims_support_claims_payment_path %> + <%= govuk_button_link_to t(".buttons.upload_esfa_response"), new_claims_support_claims_payment_response_path, secondary: true %>
    <% if @claims.any? %> diff --git a/config/analytics.yml b/config/analytics.yml index f44101c47..3940684ff 100644 --- a/config/analytics.yml +++ b/config/analytics.yml @@ -89,6 +89,7 @@ shared: - claim_window_id - sampling_reason - payment_in_progress_at + - unpaid_reason :samplings: - id - created_at @@ -236,6 +237,12 @@ shared: - claim_id - created_at - updated_at + :payment_responses: + - id + - user_id + - processed + - created_at + - updated_at :claim_activities: - id - action diff --git a/config/locales/en/activemodel.yml b/config/locales/en/activemodel.yml index 00a9d470c..146104241 100644 --- a/config/locales/en/activemodel.yml +++ b/config/locales/en/activemodel.yml @@ -164,6 +164,10 @@ en: blank: Enter a window closing date overlap: Select a date that is not within an existing claim window greater_than_or_equal_to: Enter a window closing date that is after the opening date + claims/support/claims/payment_response_form: + attributes: + csv_file: + blank: Select a CSV file to upload user_invite_form: attributes: email: diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index 6e78fa326..58267be92 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -32,6 +32,12 @@ en: sampling_response_uploaded: Provider sampling response uploaded clawback_request_delivered: Claims sent to ESFA for clawback clawback_response_uploaded: ESFA clawback response uploaded + claims/claim_activity/document: + payment_request_delivered: Claims sent to ESFA + payment_response_uploaded: ESFA payment response + sampling_response_uploaded: Provider sampling response + clawback_request_delivered: Claims sent to ESFA + clawback_response_uploaded: ESFA clawback response claims/claim_window: academic_year: Academic year window: Claim window @@ -74,6 +80,10 @@ en: greater_than: Enter the number of hours between 1 and 20 less_than_or_equal_to: Enter the number of hours between 1 and 20 not_an_integer: Enter whole numbers only + claims/payment_response: + attributes: + csv_file: + blank: Select a CSV file to upload user: attributes: first_name: diff --git a/config/locales/en/claims/support/claims/payment_responses.yml b/config/locales/en/claims/support/claims/payment_responses.yml new file mode 100644 index 000000000..17ca01982 --- /dev/null +++ b/config/locales/en/claims/support/claims/payment_responses.yml @@ -0,0 +1,41 @@ +en: + claims: + support: + claims: + payment_responses: + new: + page_caption: Payments + page_title: Upload ESFA response + label: Upload CSV file + help: + label: Help with the CSV file + description: Use this form to upload the CSV file sent by the ESFA. + csv_instructions: "The CSV file must contain the following headers in the first row:" + csv_headers: + - claim_reference + - school_urn + - school_name + - school_local_authority + - school_establishment_type + - school_establishment_type_group + - claim_amount + - claim_submission_date + - claim_status + - claim_unpaid_reason + submit: Upload CSV file + cancel: Cancel + new_not_permitted: + page_caption: Payments + page_title: You cannot upload a response from the ESFA + description: You cannot upload a response from the ESFA as there are no claims waiting for a response. + cancel: Cancel + check: + page_caption: Payments + page_title: Are you sure you want to upload the ESFA response? + description: + one: There is %{count} claim included in this upload. + other: There are %{count} claims included in this upload. + submit: Upload response + cancel: Cancel + update: + success: ESFA response uploaded diff --git a/config/locales/en/claims/support/claims/payments.yml b/config/locales/en/claims/support/claims/payments.yml index 69a6bacca..4b1ca93ce 100644 --- a/config/locales/en/claims/support/claims/payments.yml +++ b/config/locales/en/claims/support/claims/payments.yml @@ -11,6 +11,7 @@ en: other: Payments (%{count}) buttons: send_claims_to_esfa: Send claims to ESFA + upload_esfa_response: Upload ESFA response description: one: "%{count} claim needs processing" other: "%{count} claims need processing" diff --git a/config/locales/en/claims/support/claims/payments/claims.yml b/config/locales/en/claims/support/claims/payments/claims.yml index 964f908e1..741aee2ae 100644 --- a/config/locales/en/claims/support/claims/payments/claims.yml +++ b/config/locales/en/claims/support/claims/payments/claims.yml @@ -12,6 +12,7 @@ en: mentor_with_index: Mentor %{index} mentor: Mentor submitted_by: Submitted by %{name} on %{date}. + unpaid_reason_header: Reason claim was not paid buttons: information_sent: Confirm information sent paid: Confirm claim paid diff --git a/config/routes/claims.rb b/config/routes/claims.rb index c7e465529..1cb2f2494 100644 --- a/config/routes/claims.rb +++ b/config/routes/claims.rb @@ -106,6 +106,10 @@ end resources :payments, only: %i[index new create] + resources :payment_responses, only: %i[new update] do + post :check, on: :collection + end + resources :samplings, path: "sampling/claims", only: %i[index show] do member do get :confirm_approval diff --git a/config/scheduled_jobs.yml b/config/scheduled_jobs.yml index 97c6c6f4c..8f8bf1e5d 100644 --- a/config/scheduled_jobs.yml +++ b/config/scheduled_jobs.yml @@ -31,6 +31,11 @@ remove_internal_draft_claims: class: "Claims::RemoveInternalDraftClaimsJob" description: "Remove all internal_draft claims" +remove_unprocessed_claims_payment_responses: + cron: "20 0 * * *" # Every day at 00:20. + class: "Claims::RemoveUnprocessedPaymentResponsesJob" + description: "Remove unprocessed claims payment responses" + send_entity_table_checks_to_bigquery: cron: "30 0 * * *" # Every day at 00:30. class: "DfE::Analytics::EntityTableCheckJob" diff --git a/db/migrate/20241224133333_create_claims_payment_responses.rb b/db/migrate/20241224133333_create_claims_payment_responses.rb new file mode 100644 index 000000000..b1c8c6ec4 --- /dev/null +++ b/db/migrate/20241224133333_create_claims_payment_responses.rb @@ -0,0 +1,10 @@ +class CreateClaimsPaymentResponses < ActiveRecord::Migration[7.2] + def change + create_table :payment_responses, id: :uuid do |t| + t.references :user, null: false, foreign_key: true, type: :uuid + t.boolean :processed, default: false + + t.timestamps + end + end +end diff --git a/db/migrate/20250102102324_add_unpaid_reason_to_claims.rb b/db/migrate/20250102102324_add_unpaid_reason_to_claims.rb new file mode 100644 index 000000000..b3ec5b722 --- /dev/null +++ b/db/migrate/20250102102324_add_unpaid_reason_to_claims.rb @@ -0,0 +1,5 @@ +class AddUnpaidReasonToClaims < ActiveRecord::Migration[7.2] + def change + add_column :claims, :unpaid_reason, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 128a2cdfa..82079c6a9 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.2].define(version: 2024_12_20_141331) do +ActiveRecord::Schema[7.2].define(version: 2025_01_02_102324) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" enable_extension "plpgsql" @@ -122,6 +122,7 @@ t.uuid "claim_window_id" t.text "sampling_reason" t.datetime "payment_in_progress_at" + t.text "unpaid_reason" t.index ["claim_window_id"], name: "index_claims_on_claim_window_id" t.index ["created_by_type", "created_by_id"], name: "index_claims_on_created_by" t.index ["previous_revision_id"], name: "index_claims_on_previous_revision_id" @@ -301,6 +302,14 @@ t.index ["payment_id"], name: "index_payment_claims_on_payment_id" end + create_table "payment_responses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "user_id", null: false + t.boolean "processed", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_payment_responses_on_user_id" + end + create_table "payments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "sent_by_id", null: false t.datetime "created_at", null: false @@ -564,6 +573,7 @@ add_foreign_key "partnerships", "schools" add_foreign_key "payment_claims", "claims" add_foreign_key "payment_claims", "payments" + add_foreign_key "payment_responses", "users" add_foreign_key "payments", "users", column: "sent_by_id" add_foreign_key "placement_additional_subjects", "placements" add_foreign_key "placement_additional_subjects", "subjects" diff --git a/spec/factories/claims.rb b/spec/factories/claims.rb index 52d6c6ac9..5c37d64cc 100644 --- a/spec/factories/claims.rb +++ b/spec/factories/claims.rb @@ -11,6 +11,7 @@ # status :enum # submitted_at :datetime # submitted_by_type :string +# unpaid_reason :text # created_at :datetime not null # updated_at :datetime not null # claim_window_id :uuid diff --git a/spec/factories/claims/payment_responses.rb b/spec/factories/claims/payment_responses.rb new file mode 100644 index 000000000..142afaaf0 --- /dev/null +++ b/spec/factories/claims/payment_responses.rb @@ -0,0 +1,25 @@ +# == Schema Information +# +# Table name: payment_responses +# +# id :uuid not null, primary key +# processed :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# user_id :uuid not null +# +# Indexes +# +# index_payment_responses_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +FactoryBot.define do + factory :claims_payment_response, class: "Claims::PaymentResponse" do + association :user, factory: :claims_support_user + + csv_file { file_fixture("example-payments-response.csv") } + end +end diff --git a/spec/factories/claims/payments.rb b/spec/factories/claims/payments.rb index 9d2155da1..d4f9e7c7e 100644 --- a/spec/factories/claims/payments.rb +++ b/spec/factories/claims/payments.rb @@ -20,6 +20,6 @@ association :sent_by, factory: :claims_support_user claims { build_list(:claim, 3, :submitted) } - csv_file { file_fixture("example.csv") } + csv_file { file_fixture("example-payments.csv") } end end diff --git a/spec/factories/claims/provider_samplings.rb b/spec/factories/claims/provider_samplings.rb index a3b7d58ce..6cf24f774 100644 --- a/spec/factories/claims/provider_samplings.rb +++ b/spec/factories/claims/provider_samplings.rb @@ -23,6 +23,6 @@ association :sampling, factory: :claims_sampling association :provider, factory: :claims_provider - csv_file { file_fixture("example.csv") } + csv_file { file_fixture("example-sampling-response.csv") } end end diff --git a/spec/fixtures/files/example-payments-response.csv b/spec/fixtures/files/example-payments-response.csv new file mode 100644 index 000000000..93be7f021 --- /dev/null +++ b/spec/fixtures/files/example-payments-response.csv @@ -0,0 +1,6 @@ +claim_reference,school_urn,school_name,claim_status,claim_unpaid_reason +12345678,1234567,Example School 1,paid +23456789,2345678,Example School 2,unpaid,A reason +34567890,2345678,Example School 2,paid +45678901,1234567,Example School 1,something_else +56789012,1234567,Example School 1,paid diff --git a/spec/fixtures/files/example-payments.csv b/spec/fixtures/files/example-payments.csv new file mode 100644 index 000000000..fbdb1c550 --- /dev/null +++ b/spec/fixtures/files/example-payments.csv @@ -0,0 +1,3 @@ +claim_reference,school_urn,school_name,claim_status,claim_unpaid_reason +12345678,1234567,Example School,submitted +23456789,2345678,Example School 2,submitted diff --git a/spec/fixtures/files/example-sampling-response.csv b/spec/fixtures/files/example-sampling-response.csv new file mode 100644 index 000000000..5aafed7ff --- /dev/null +++ b/spec/fixtures/files/example-sampling-response.csv @@ -0,0 +1,4 @@ +claim_reference,mentor_full_name,claim_assured,claim_not_assured_reason +11111111,John Smith,true,Some reason +11111111,Jane Doe,false,Another reason +22222222,Joe Bloggs,true,Yet another reason diff --git a/spec/fixtures/files/example-sampling.csv b/spec/fixtures/files/example-sampling.csv new file mode 100644 index 000000000..478447525 --- /dev/null +++ b/spec/fixtures/files/example-sampling.csv @@ -0,0 +1,2 @@ +claim_reference,sample_reason +11111111,Some Reason diff --git a/spec/fixtures/files/example.csv b/spec/fixtures/files/example.csv deleted file mode 100644 index 53dad8ec9..000000000 --- a/spec/fixtures/files/example.csv +++ /dev/null @@ -1,2 +0,0 @@ -claim_reference,school_urn,school_name,claim_status -12345678,1234567,Example School,paid diff --git a/spec/jobs/claims/remove_unprocessed_payment_responses_job_spec.rb b/spec/jobs/claims/remove_unprocessed_payment_responses_job_spec.rb new file mode 100644 index 000000000..3c9cd02e9 --- /dev/null +++ b/spec/jobs/claims/remove_unprocessed_payment_responses_job_spec.rb @@ -0,0 +1,34 @@ +require "rails_helper" + +RSpec.describe Claims::RemoveUnprocessedPaymentResponsesJob, type: :job do + describe "queue name" do + subject { described_class.new.queue_name } + + it { is_expected.to eq("default") } + end + + describe "#perform" do + subject(:perform) { described_class.perform_now } + + let!(:processed_payment_response) { create(:claims_payment_response, processed: true, created_at: 1.day.ago) } + + it "does nothing" do + expect { perform }.not_to change(Claims::PaymentResponse, :count) + + expect { processed_payment_response.reload }.not_to raise_error + end + + context "when there are old unprocessed claims payment responses" do + let!(:old_unprocessed_payment_response) { create(:claims_payment_response, created_at: 1.day.ago) } + let!(:recent_unprocessed_payment_response) { create(:claims_payment_response, created_at: Time.current) } + + it "deletes unprocessed claim payment responses over a day old" do + expect { perform }.to change(Claims::PaymentResponse, :count).by(-1) + + expect { old_unprocessed_payment_response.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { recent_unprocessed_payment_response.reload }.not_to raise_error + expect { processed_payment_response.reload }.not_to raise_error + end + end + end +end diff --git a/spec/models/claims/claim_spec.rb b/spec/models/claims/claim_spec.rb index db654f928..be903c1c7 100644 --- a/spec/models/claims/claim_spec.rb +++ b/spec/models/claims/claim_spec.rb @@ -11,6 +11,7 @@ # status :enum # submitted_at :datetime # submitted_by_type :string +# unpaid_reason :text # created_at :datetime not null # updated_at :datetime not null # claim_window_id :uuid diff --git a/spec/models/claims/payment_response_spec.rb b/spec/models/claims/payment_response_spec.rb new file mode 100644 index 000000000..f366baa0e --- /dev/null +++ b/spec/models/claims/payment_response_spec.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: payment_responses +# +# id :uuid not null, primary key +# processed :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# user_id :uuid not null +# +# Indexes +# +# index_payment_responses_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +require "rails_helper" + +RSpec.describe Claims::PaymentResponse, type: :model do + describe "associations" do + it { is_expected.to belong_to(:user) } + it { is_expected.to have_one_attached(:csv_file) } + end +end diff --git a/spec/models/claims/payment_spec.rb b/spec/models/claims/payment_spec.rb index 023386b5c..60347bd56 100644 --- a/spec/models/claims/payment_spec.rb +++ b/spec/models/claims/payment_spec.rb @@ -22,5 +22,6 @@ it { is_expected.to belong_to(:sent_by).class_name("Claims::SupportUser") } it { is_expected.to have_many(:payment_claims).dependent(:destroy) } it { is_expected.to have_many(:claims).through(:payment_claims) } + it { is_expected.to have_one_attached(:csv_file) } end end diff --git a/spec/services/claims/payment_response/process_spec.rb b/spec/services/claims/payment_response/process_spec.rb new file mode 100644 index 000000000..6f88cad4b --- /dev/null +++ b/spec/services/claims/payment_response/process_spec.rb @@ -0,0 +1,30 @@ +require "rails_helper" + +describe Claims::PaymentResponse::Process do + subject(:process) { described_class.call(payment_response:, current_user:) } + + let(:payment_response) { create(:claims_payment_response, csv_file: file_fixture("example-payments-response.csv")) } + let(:current_user) { create(:claims_support_user) } + + describe "#call" do + it "does nothing when there are no claims with payment in progress" do + expect { process }.to not_change(Claims::ClaimActivity, :count) + end + + context "when there are claims with payment in progress" do + let(:approved_claim) { create(:claim, :payment_in_progress, reference: "12345678") } + let(:rejected_claim) { create(:claim, :payment_in_progress, reference: "23456789") } + let(:paid_claim) { create(:claim, :paid, reference: "34567890") } + let(:ignored_claim) { create(:claim, :payment_in_progress, reference: "45678901") } + + it "creates a payment, activity, updates claims statuses to 'payment_in_progress', and enqueues the deliver of an email to the ESFA" do + expect { process }.to change(Claims::ClaimActivity, :count).by(1) + .and change { approved_claim.reload.status }.from("payment_in_progress").to("paid") + .and change { rejected_claim.reload.status }.from("payment_in_progress").to("payment_information_requested") + .and change { rejected_claim.reload.unpaid_reason }.from(nil).to("A reason") + .and(not_change { paid_claim.reload.status }) + .and(not_change { ignored_claim.reload.status }) + end + end + end +end diff --git a/spec/system/claims/support/claims/payments/upload_esfa_response_spec.rb b/spec/system/claims/support/claims/payments/upload_esfa_response_spec.rb new file mode 100644 index 000000000..a75656abe --- /dev/null +++ b/spec/system/claims/support/claims/payments/upload_esfa_response_spec.rb @@ -0,0 +1,105 @@ +require "rails_helper" + +RSpec.describe "Upload ESFA response CSV", service: :claims, type: :system do + let(:support_user) { create(:claims_support_user) } + + scenario "Support user attempts to upload ESFA response, but there are no 'payment_in_progress' claims" do + given_i_sign_in + when_i_visit_claims_payments_index_page + when_i_click_on_upload_esfa_response + then_i_can_see_an_error_page + end + + context "when there are claims with payment in progress" do + before do + create(:claim, :payment_in_progress, reference: "12345678") + create(:claim, :payment_in_progress, reference: "23456789") + end + + scenario "Support user sends claims to ESFA" do + given_i_sign_in + when_i_visit_claims_payments_index_page + when_i_click_on_upload_esfa_response + then_i_can_see_an_upload_form + + when_i_attach_a_csv_file + and_i_click_on_upload_csv_file + then_i_can_see_a_confirmation_page + + when_i_click_on_upload_response + then_i_see_a_success_flash_message + and_claims_have_been_paid_and_payment_information_requested + end + + scenario "Support user attempts to upload ESFA response without attaching a file" do + given_i_sign_in + when_i_visit_claims_payments_index_page + when_i_click_on_upload_esfa_response + then_i_can_see_an_upload_form + + when_i_click_on_upload_csv_file + then_i_see_the_error_message("Select a CSV file to upload") + end + end + + private + + def given_i_sign_in + sign_in_as(support_user) + end + + def when_i_visit_claims_payments_index_page + click_on("Claims") + click_on("Payments") + end + + def when_i_click_on_upload_esfa_response + click_on("Upload ESFA response") + end + + def then_i_can_see_an_upload_form + expect(page).to have_field("CSV file") + end + + def then_i_can_see_an_error_page + expect(page).to have_css("p.govuk-caption-l", text: "Payments") + expect(page).to have_css("h1.govuk-heading-l", text: "You cannot upload a response from the ESFA") + expect(page).to have_css("p.govuk-body", text: "You cannot upload a response from the ESFA as there are no claims waiting for a response.") + expect(page).to have_link("Cancel", href: claims_support_claims_payments_path) + end + + def when_i_attach_a_csv_file + attach_file "CSV file", file_fixture("example-payments-response.csv") + end + + def when_i_click_on_upload_csv_file + click_on "Upload CSV file" + end + alias_method :and_i_click_on_upload_csv_file, :when_i_click_on_upload_csv_file + + def then_i_can_see_a_confirmation_page + expect(page).to have_css("p.govuk-caption-l", text: "Payments") + expect(page).to have_css("h1.govuk-heading-l", text: "Are you sure you want to upload the ESFA response?") + expect(page).to have_css("p.govuk-body", text: "There are 5 claims included in this upload.") + end + + def when_i_click_on_upload_response + click_on("Upload response") + end + + def then_i_see_a_success_flash_message + expect(page).to have_content("ESFA response uploaded") + end + + def then_i_see_the_error_message(message) + within(".govuk-error-summary") do + expect(page).to have_content(message) + end + end + + def and_claims_have_been_paid_and_payment_information_requested + expect(Claims::Claim.payment_in_progress.count).to eq(0) + expect(Claims::Claim.paid.count).to eq(1) + expect(Claims::Claim.payment_information_requested.count).to eq(1) + end +end diff --git a/spec/system/claims/support/claims/payments/view_a_claim_spec.rb b/spec/system/claims/support/claims/payments/view_a_claim_spec.rb index fe230f20b..6b3af62d9 100644 --- a/spec/system/claims/support/claims/payments/view_a_claim_spec.rb +++ b/spec/system/claims/support/claims/payments/view_a_claim_spec.rb @@ -3,7 +3,7 @@ RSpec.describe "View claims", service: :claims, type: :system do let!(:support_user) { create(:claims_support_user) } - let!(:claim) { create(:claim, :payment_information_requested) } + let!(:claim) { create(:claim, :payment_information_requested, unpaid_reason: "Some reason") } before do user_exists_in_dfe_sign_in(user: support_user) @@ -38,5 +38,9 @@ def then_i_can_see_the_details_of_claim(claim) expect(page).to have_content("School#{claim.school_name}") expect(page).to have_content("Academic year#{claim.academic_year_name}") expect(page).to have_content("Accredited provider#{claim.provider.name}") + + within(".govuk-inset-text") do + expect(page).to have_content("Some reason") + end end end