diff --git a/.env.test b/.env.test
index 5df3ed04a..edaead8f8 100644
--- a/.env.test
+++ b/.env.test
@@ -8,6 +8,7 @@ CLAIMS_HOST=claims.localhost
CLAIMS_HOSTS=claims.localhost
CLAIMS_DFE_SIGN_IN_CLIENT_ID=123
CLAIMS_DFE_SIGN_IN_SECRET=secret
+CLAIMS_ESFA_EMAIL_ADDRESSES=esfa@example.com
PLACEMENTS_HOST=placements.localhost
PLACEMENTS_HOSTS=placements.localhost
diff --git a/app/controllers/claims/payments/claims_controller.rb b/app/controllers/claims/payments/claims_controller.rb
new file mode 100644
index 000000000..e12490c8f
--- /dev/null
+++ b/app/controllers/claims/payments/claims_controller.rb
@@ -0,0 +1,29 @@
+class Claims::Payments::ClaimsController < Claims::ApplicationController
+ skip_before_action :authenticate_user!
+
+ before_action :skip_authorization
+ before_action :validate_token
+ before_action :set_payment
+
+ def download
+ send_data @payment.csv_file.download, filename: "payments-claims-#{Time.current.iso8601}.csv"
+ end
+
+ private
+
+ def validate_token
+ @payment_id = Rails.application.message_verifier(:payments).verify(token_param)
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ render "error"
+ end
+
+ def set_payment
+ @payment = Claims::Payment.find(@payment_id)
+ rescue ActiveRecord::RecordNotFound
+ render "error"
+ end
+
+ def token_param
+ params.fetch(:token, nil)
+ end
+end
diff --git a/app/controllers/claims/support/claims/payments/claims_controller.rb b/app/controllers/claims/support/claims/payments/claims_controller.rb
index 40fdb84dd..d5691554d 100644
--- a/app/controllers/claims/support/claims/payments/claims_controller.rb
+++ b/app/controllers/claims/support/claims/payments/claims_controller.rb
@@ -1,4 +1,6 @@
class Claims::Support::Claims::Payments::ClaimsController < Claims::Support::ApplicationController
+ append_pundit_namespace :claims, :payments
+
before_action :set_claim, only: %i[show]
before_action :authorize_claim
@@ -11,6 +13,6 @@ def set_claim
end
def authorize_claim
- authorize [:payments, @claim || Claims::Claim]
+ authorize @claim || Claims::Claim
end
end
diff --git a/app/controllers/claims/support/claims/payments_controller.rb b/app/controllers/claims/support/claims/payments_controller.rb
index fa92c44e9..fc2588b18 100644
--- a/app/controllers/claims/support/claims/payments_controller.rb
+++ b/app/controllers/claims/support/claims/payments_controller.rb
@@ -1,4 +1,6 @@
class Claims::Support::Claims::PaymentsController < Claims::Support::ApplicationController
+ append_pundit_namespace :claims
+
before_action :authorize_claims
helper_method :filter_form
@@ -7,6 +9,20 @@ def index
@pagy, @claims = pagy(filtered_claims)
end
+ def new
+ @submitted_claims = Claims::Claim.submitted
+
+ unless policy(Claims::Payment).create?
+ render "new_not_permitted"
+ end
+ end
+
+ def create
+ Claims::Payment::CreateAndDeliver.call(current_user:)
+
+ redirect_to claims_support_claims_payments_path, flash: { success: true, heading: "Claims sent to ESFA" }
+ end
+
private
def filtered_claims
@@ -40,6 +56,6 @@ def filter_params
end
def authorize_claims
- authorize filtered_claims
+ authorize [:payments, filtered_claims]
end
end
diff --git a/app/mailers/claims/payment_mailer.rb b/app/mailers/claims/payment_mailer.rb
new file mode 100644
index 000000000..65710d41c
--- /dev/null
+++ b/app/mailers/claims/payment_mailer.rb
@@ -0,0 +1,24 @@
+class Claims::PaymentMailer < Claims::ApplicationMailer
+ def payment_created_notification(payment)
+ @payment = payment
+
+ notify_email to: esfa_email_addresses,
+ from: support_email,
+ subject: t(".subject"),
+ body: t(
+ ".body",
+ download_page_url: claims_payments_claims_url(token:),
+ support_email:,
+ )
+ end
+
+ private
+
+ def esfa_email_addresses
+ ENV.fetch("CLAIMS_ESFA_EMAIL_ADDRESSES").split(",")
+ end
+
+ def token
+ Rails.application.message_verifier(:payments).generate(@payment.id, expires_in: 7.days)
+ end
+end
diff --git a/app/models/claims/payment.rb b/app/models/claims/payment.rb
index 36fd66cd8..783fcfefc 100644
--- a/app/models/claims/payment.rb
+++ b/app/models/claims/payment.rb
@@ -19,5 +19,7 @@ class Claims::Payment < ApplicationRecord
belongs_to :sent_by, class_name: "Claims::SupportUser"
has_many :payment_claims, dependent: :destroy
- has_many :claims, through: :payment_claims
+ has_many :claims, through: :payment_claims, class_name: "Claims::Claim"
+
+ has_one_attached :csv_file
end
diff --git a/app/policies/claims/support/claims/payment_policy.rb b/app/policies/claims/support/claims/payment_policy.rb
new file mode 100644
index 000000000..b7ae94b5c
--- /dev/null
+++ b/app/policies/claims/support/claims/payment_policy.rb
@@ -0,0 +1,9 @@
+class Claims::Support::Claims::PaymentPolicy < Claims::ApplicationPolicy
+ def new?
+ true
+ end
+
+ def create?
+ Claims::Claim.submitted.any?
+ end
+end
diff --git a/app/policies/claims/support/claims/payments/claim_policy.rb b/app/policies/claims/support/claims/payments/claim_policy.rb
new file mode 100644
index 000000000..6ebfe3477
--- /dev/null
+++ b/app/policies/claims/support/claims/payments/claim_policy.rb
@@ -0,0 +1,5 @@
+class Claims::Support::Claims::Payments::ClaimPolicy < Claims::ApplicationPolicy
+ def update?
+ false
+ end
+end
diff --git a/app/policies/claims/support/payments/claim_policy.rb b/app/policies/claims/support/payments/claim_policy.rb
deleted file mode 100644
index 1659b7df7..000000000
--- a/app/policies/claims/support/payments/claim_policy.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class Claims::Support::Payments::ClaimPolicy < Claims::ApplicationPolicy
-end
diff --git a/app/services/claims/payment/create_and_deliver.rb b/app/services/claims/payment/create_and_deliver.rb
new file mode 100644
index 000000000..d3887df5a
--- /dev/null
+++ b/app/services/claims/payment/create_and_deliver.rb
@@ -0,0 +1,33 @@
+class Claims::Payment::CreateAndDeliver < ApplicationService
+ def initialize(current_user:)
+ @current_user = current_user
+ end
+
+ def call
+ return if submitted_claims.none?
+
+ ActiveRecord::Base.transaction do |transaction|
+ csv_file = Claims::Payments::Claim::GenerateCSVFile.call(claims: submitted_claims)
+
+ payment = Claims::Payment.create!(sent_by: current_user, csv_file: File.open(csv_file.to_io), claims: submitted_claims)
+
+ submitted_claims.find_each do |claim|
+ claim.update!(status: :payment_in_progress, payment_in_progress_at: Time.current)
+ end
+
+ Claims::ClaimActivity.create!(action: :payment_delivered, user: current_user, record: payment)
+
+ transaction.after_commit do
+ Claims::PaymentMailer.payment_created_notification(payment).deliver_later
+ end
+ end
+ end
+
+ private
+
+ attr_reader :current_user
+
+ def submitted_claims
+ @submitted_claims ||= Claims::Claim.submitted
+ end
+end
diff --git a/app/services/claims/payments/claim/generate_csv_file.rb b/app/services/claims/payments/claim/generate_csv_file.rb
new file mode 100644
index 000000000..80931b2f7
--- /dev/null
+++ b/app/services/claims/payments/claim/generate_csv_file.rb
@@ -0,0 +1,50 @@
+require "csv"
+
+class Claims::Payments::Claim::GenerateCSVFile < ApplicationService
+ HEADERS = %w[
+ claim_reference
+ school_urn
+ school_name
+ school_local_authority
+ claim_amount
+ school_type_of_establishment
+ school_group
+ claim_submission_date
+ claim_status
+ claim_unpaid_reason
+ ].freeze
+
+ def initialize(claims:)
+ @claims = claims
+ end
+
+ def call
+ CSV.open(file_name, "w", headers: true) do |csv|
+ csv << HEADERS
+
+ claims.each do |claim|
+ csv << [
+ claim.reference,
+ claim.school.urn,
+ claim.school_name,
+ claim.school.local_authority_name,
+ claim.amount.format(symbol: false, decimal_mark: ".", no_cents: false),
+ claim.school.type_of_establishment,
+ claim.school.group,
+ claim.submitted_at&.iso8601,
+ claim.status,
+ ]
+ end
+
+ csv
+ end
+ end
+
+ private
+
+ attr_reader :claims
+
+ def file_name
+ Rails.root.join("tmp/payment-claims-#{Time.current}.csv")
+ end
+end
diff --git a/app/views/claims/payments/claims/error.html.erb b/app/views/claims/payments/claims/error.html.erb
new file mode 100644
index 000000000..446d1209f
--- /dev/null
+++ b/app/views/claims/payments/claims/error.html.erb
@@ -0,0 +1,11 @@
+
+
+
+
<%= t(".page_title") %>
+
+
<%= t(".description") %>
+
+
<%= t(".support_html", mail_to: govuk_mail_to(t("claims.support_email"), t("claims.support_email_html"))) %>
+
+
+
diff --git a/app/views/claims/payments/claims/index.html.erb b/app/views/claims/payments/claims/index.html.erb
new file mode 100644
index 000000000..035244e16
--- /dev/null
+++ b/app/views/claims/payments/claims/index.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
<%= t(".page_title") %>
+
+
<%= t(".description_html") %>
+
+
<%= t(".support_html", mail_to: govuk_mail_to(t("claims.support_email"), t("claims.support_email_html"))) %>
+
+ <%= govuk_button_link_to t(".submit"), download_claims_payments_claims_path(token: params[:token]) %>
+
+
+
diff --git a/app/views/claims/support/claims/payments/index.html.erb b/app/views/claims/support/claims/payments/index.html.erb
index fc9d85b38..d20fa4780 100644
--- a/app/views/claims/support/claims/payments/index.html.erb
+++ b/app/views/claims/support/claims/payments/index.html.erb
@@ -8,6 +8,10 @@
<%= t(".sub_heading", count: @pagy.count) %>
+
+ <%= govuk_button_link_to t(".buttons.send_claims_to_esfa"), new_claims_support_claims_payment_path %>
+
+
<% if @claims.any? %>
<%= t(".description", count: @pagy.count) %>
diff --git a/app/views/claims/support/claims/payments/new.html.erb b/app/views/claims/support/claims/payments/new.html.erb
new file mode 100644
index 000000000..4463882ea
--- /dev/null
+++ b/app/views/claims/support/claims/payments/new.html.erb
@@ -0,0 +1,31 @@
+<%= 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", count: @submitted_claims.count) %>
+
+
<%= t(".details.heading") %>
+
+ <%= govuk_list type: :bullet do %>
+ <% t(".details.list").each do |item| %>
+
<%= item %>
+ <% end %>
+ <% end %>
+
+ <%= govuk_warning_text text: t(".warning") %>
+
+ <%= govuk_button_to t(".submit"), claims_support_claims_payments_path %>
+
+
<%= govuk_link_to t(".cancel"), claims_support_claims_payments_path %>
+
+
+
diff --git a/app/views/claims/support/claims/payments/new_not_permitted.html.erb b/app/views/claims/support/claims/payments/new_not_permitted.html.erb
new file mode 100644
index 000000000..4b7f5d5d0
--- /dev/null
+++ b/app/views/claims/support/claims/payments/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/config/application.rb b/config/application.rb
index d84457ce7..3c4c2a4c9 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -55,5 +55,11 @@ class Application < Rails::Application
# Store user sessions in the database
config.session_store :active_record_store
+
+ # Configure message verifiers to generate URL-safe tokens.
+ config.before_initialize do |app|
+ app.message_verifiers.clear_rotations
+ app.message_verifiers.rotate(url_safe: true)
+ end
end
end
diff --git a/config/locales/en/claims/payment_mailer.yml b/config/locales/en/claims/payment_mailer.yml
new file mode 100644
index 000000000..1caab91fb
--- /dev/null
+++ b/config/locales/en/claims/payment_mailer.yml
@@ -0,0 +1,30 @@
+en:
+ claims:
+ payment_mailer:
+ payment_created_notification:
+ subject: Claims ready for payment - Claim funding for mentor training
+ body: |
+ To ESFA,
+
+ These claims from the Claim funding for mentor training service (Claim) are ready for payment — the link to the latest CSV file is valid for 7 days:
+
+ [%{download_page_url}](%{download_page_url})
+
+ What you need to do:
+
+ 1. Check and validate the claims in the CSV file by marking them as ‘paid’ or ‘unpaid’ in the ‘claim_status’ column.
+
+ 2. If you mark a claim as ‘unpaid’, add the reason in the ‘claim_unpaid_reason’ column.
+
+ 3. Reply to this email and attach the updated CSV file.
+
+ The Claim Support team will follow up on the reasons for unpaid claims and email back an updated version.
+
+ If you have a problem opening the link or it has expired, reply to this email and request that it be sent again.
+
+ # Give feedback or report a problem
+
+ If you have any questions or feedback, please contact the team at [%{support_email}](%{support_email}).
+
+ Regards
+ Claim funding for mentor training team
diff --git a/config/locales/en/claims/payments/claims.yml b/config/locales/en/claims/payments/claims.yml
new file mode 100644
index 000000000..c79f75f4f
--- /dev/null
+++ b/config/locales/en/claims/payments/claims.yml
@@ -0,0 +1,13 @@
+en:
+ claims:
+ payments:
+ claims:
+ index:
+ page_title: Download payments file
+ description_html: Download the Claim funding for mentor training payments CSV file.
+ support_html: If you have any questions, email %{mail_to}.
+ submit: Download CSV file
+ error:
+ page_title: Sorry, there is a problem with the download link
+ description: You are seeing this page because the download link is not working. It may have timed out or contained an invalid security token.
+ support_html: Email %{mail_to} to request a new download link.
diff --git a/config/locales/en/claims/support/claims/payments.yml b/config/locales/en/claims/support/claims/payments.yml
index 27076ae04..69a6bacca 100644
--- a/config/locales/en/claims/support/claims/payments.yml
+++ b/config/locales/en/claims/support/claims/payments.yml
@@ -9,7 +9,29 @@ en:
zero: Payments
one: Payments (%{count})
other: Payments (%{count})
+ buttons:
+ send_claims_to_esfa: Send claims to ESFA
description:
one: "%{count} claim needs processing"
other: "%{count} claims need processing"
no_claims: There are no claims waiting to be processed.
+ new:
+ page_caption: Payments
+ page_title: Send claims to ESFA
+ description:
+ one: There is %{count} claim included in this submission.
+ other: There are %{count} claims included in this submission.
+ details:
+ heading: "Selecting ‘Send claims’ will:"
+ list:
+ - create a CSV containing a list of all ‘Submitted’ claims
+ - send an email to the ESFA containing a link to the generated CSV - this link expires after 7 days
+ - update the claim status from ‘Submitted’ to ‘Payment in progress’
+ warning: This action cannot be undone.
+ submit: Send claims
+ cancel: Cancel
+ new_not_permitted:
+ page_caption: Payments
+ page_title: There are no claims to send for payment
+ description: You cannot send any claims to the ESFA because there are no claims pending payment.
+ cancel: Cancel
diff --git a/config/routes/claims.rb b/config/routes/claims.rb
index e5a8c93b0..a5c0b7df4 100644
--- a/config/routes/claims.rb
+++ b/config/routes/claims.rb
@@ -13,6 +13,12 @@
resources :service_updates, path: "service-updates", only: %i[index show]
+ namespace :payments do
+ resources :claims, only: %i[index] do
+ get :download, on: :collection
+ end
+ end
+
resources :schools, only: %i[index show] do
scope module: :schools do
resources :claims do
@@ -76,12 +82,13 @@
resources :claims, only: %i[show]
end
- resources :payments, only: %i[index]
+ resources :payments, only: %i[index new create]
resources :samplings, path: "sampling/claims", only: %i[index show] do
member do
get :confirm_approval
put :update
end
+
collection do
get "new", to: "samplings/upload_data#new", as: :new_upload_data
get "new/:state_key/:step", to: "samplings/upload_data#edit", as: :upload_data
diff --git a/spec/factories/claims/payments.rb b/spec/factories/claims/payments.rb
index 415809f44..b4e85bee7 100644
--- a/spec/factories/claims/payments.rb
+++ b/spec/factories/claims/payments.rb
@@ -18,5 +18,7 @@
FactoryBot.define do
factory :claims_payment, class: "Claims::Payment" do
association :sent_by, factory: :claims_support_user
+
+ claims { build_list(:claim, 3, :submitted) }
end
end
diff --git a/spec/mailers/claims/payment_mailer_spec.rb b/spec/mailers/claims/payment_mailer_spec.rb
new file mode 100644
index 000000000..b32fe4ee0
--- /dev/null
+++ b/spec/mailers/claims/payment_mailer_spec.rb
@@ -0,0 +1,42 @@
+require "rails_helper"
+
+RSpec.describe Claims::PaymentMailer, freeze: "20 December 2024", type: :mailer do
+ describe "#payment_created_notification" do
+ subject(:email) { described_class.payment_created_notification(payment) }
+
+ let(:payment) { create(:claims_payment) }
+ let(:download_page_url) { claims_payments_claims_url(token:, host: "claims.localhost") }
+ let(:token) { Rails.application.message_verifier(:payments).generate(payment.id, expires_in: 7.days) }
+
+ it "sends the sampling checks required email" do
+ expect(email.to).to contain_exactly("esfa@example.com")
+ expect(email.subject).to eq("Claims ready for payment - Claim funding for mentor training")
+ expect(email.body.to_s.squish).to eq(<<~EMAIL.squish)
+ To ESFA,
+
+ These claims from the Claim funding for mentor training service (Claim) are ready for payment — the link to the latest CSV file is valid for 7 days:
+
+ [#{download_page_url}](#{download_page_url})
+
+ What you need to do:
+
+ 1. Check and validate the claims in the CSV file by marking them as ‘paid’ or ‘unpaid’ in the ‘claim_status’ column.
+
+ 2. If you mark a claim as ‘unpaid’, add the reason in the ‘claim_unpaid_reason’ column.
+
+ 3. Reply to this email and attach the updated CSV file.
+
+ The Claim Support team will follow up on the reasons for unpaid claims and email back an updated version.
+
+ If you have a problem opening the link or it has expired, reply to this email and request that it be sent again.
+
+ # Give feedback or report a problem
+
+ If you have any questions or feedback, please contact the team at [ittmentor.funding@education.gov.uk](ittmentor.funding@education.gov.uk).
+
+ Regards
+ Claim funding for mentor training team
+ EMAIL
+ end
+ end
+end
diff --git a/spec/mailers/previews/claims/payment_mailer_preview.rb b/spec/mailers/previews/claims/payment_mailer_preview.rb
new file mode 100644
index 000000000..05bc2e54d
--- /dev/null
+++ b/spec/mailers/previews/claims/payment_mailer_preview.rb
@@ -0,0 +1,11 @@
+class Claims::PaymentMailerPreview < ActionMailer::Preview
+ def payment_created_notification
+ Claims::PaymentMailer.payment_created_notification(payment)
+ end
+
+ private
+
+ def payment
+ Claims::Payment.order(created_at: :desc).first_or_initialize(id: SecureRandom.uuid)
+ end
+end
diff --git a/spec/policies/claims/support/claims/payment_policy_spec.rb b/spec/policies/claims/support/claims/payment_policy_spec.rb
new file mode 100644
index 000000000..c593560f6
--- /dev/null
+++ b/spec/policies/claims/support/claims/payment_policy_spec.rb
@@ -0,0 +1,25 @@
+require "rails_helper"
+
+describe Claims::Support::Claims::PaymentPolicy do
+ subject { described_class }
+
+ let(:support_user) { build(:claims_support_user) }
+
+ permissions :new? do
+ it { is_expected.to permit(support_user, Claims::Payment) }
+ end
+
+ permissions :create? do
+ context "when there are submitted claims" do
+ before do
+ create(:claim, :submitted)
+ end
+
+ it { is_expected.to permit(support_user, Claims::Payment) }
+ end
+
+ context "when there are no submitted claims" do
+ it { is_expected.not_to permit(support_user, Claims::Payment) }
+ end
+ end
+end
diff --git a/spec/policies/claims/support/claims/payments/claim_policy_spec.rb b/spec/policies/claims/support/claims/payments/claim_policy_spec.rb
new file mode 100644
index 000000000..4de6f2a68
--- /dev/null
+++ b/spec/policies/claims/support/claims/payments/claim_policy_spec.rb
@@ -0,0 +1,16 @@
+require "rails_helper"
+
+describe Claims::Support::Claims::Payments::ClaimPolicy do
+ subject { described_class }
+
+ let(:support_user) { build(:claims_support_user) }
+ let(:claim) { build(:claim) }
+
+ permissions :show? do
+ it { is_expected.to permit(support_user, claim) }
+ end
+
+ permissions :edit?, :update? do
+ it { is_expected.not_to permit(support_user, claim) }
+ end
+end
diff --git a/spec/services/claims/payment/create_and_deliver_spec.rb b/spec/services/claims/payment/create_and_deliver_spec.rb
new file mode 100644
index 000000000..a88e76efb
--- /dev/null
+++ b/spec/services/claims/payment/create_and_deliver_spec.rb
@@ -0,0 +1,28 @@
+require "rails_helper"
+
+describe Claims::Payment::CreateAndDeliver do
+ subject(:create_and_deliver) { described_class.call(current_user:) }
+
+ let(:current_user) { create(:claims_support_user) }
+
+ describe "#call" do
+ it "does nothing when there are no submitted claims" do
+ expect { create_and_deliver }.to not_change(Claims::Payment, :count)
+ .and not_change(Claims::ClaimActivity, :count)
+ .and not_enqueue_mail(Claims::PaymentMailer, :payment_created_notification)
+ end
+
+ context "when there are submitted claims" do
+ before do
+ create(:claim, :submitted)
+ end
+
+ it "creates a payment, activity, updates claims statuses to 'payment_in_progress', and enqueues the deliver of an email to the ESFA" do
+ expect { create_and_deliver }.to change(Claims::Payment, :count).by(1)
+ .and change(Claims::ClaimActivity, :count).by(1)
+ .and change { Claims::Claim.pluck(:status).uniq }.from(%w[submitted]).to(%w[payment_in_progress])
+ .and enqueue_mail(Claims::PaymentMailer, :payment_created_notification)
+ end
+ end
+ end
+end
diff --git a/spec/services/claims/payments/claim/generate_csv_file_spec.rb b/spec/services/claims/payments/claim/generate_csv_file_spec.rb
new file mode 100644
index 000000000..ebd2b2cff
--- /dev/null
+++ b/spec/services/claims/payments/claim/generate_csv_file_spec.rb
@@ -0,0 +1,32 @@
+require "rails_helper"
+
+describe Claims::Payments::Claim::GenerateCSVFile do
+ subject(:generate_csv_file) { described_class.call(claims:) }
+
+ let(:claims) { create_list(:claim, 3) }
+
+ describe "#call" do
+ it "generates a CSV file of claims" do
+ expect(generate_csv_file).to be_a(CSV)
+ expect(generate_csv_file).to be_closed
+
+ csv = CSV.read(generate_csv_file.path)
+
+ expect(csv.first).to eq(%w[claim_reference school_urn school_name school_local_authority claim_amount school_type_of_establishment school_group claim_submission_date claim_status claim_unpaid_reason])
+
+ claims.each_with_index do |claim, index|
+ expect(csv[index + 1]).to eq([
+ claim.reference,
+ claim.school.urn,
+ claim.school_name,
+ claim.school.local_authority_name,
+ claim.amount.format(symbol: false, decimal_mark: ".", no_cents: false),
+ claim.school.type_of_establishment,
+ claim.school.group,
+ claim.submitted_at&.iso8601,
+ claim.status,
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index f4a34c4b6..2bfd1c737 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -45,6 +45,7 @@
# Instead, define negated versions of whatever matchers you wish to negate with
# `RSpec::Matchers.define_negated_matcher` and use `expect(...).to matcher.and matcher`.
RSpec::Matchers.define_negated_matcher :not_change, :change
+RSpec::Matchers.define_negated_matcher :not_enqueue_mail, :enqueue_mail
RSpec::Matchers.define_negated_matcher :not_have_enqueued_mail, :have_enqueued_mail
RSpec::Matchers.define_negated_matcher :not_have_enqueued_job, :have_enqueued_job
diff --git a/spec/system/claims/payments/claims/download_claims_spec.rb b/spec/system/claims/payments/claims/download_claims_spec.rb
new file mode 100644
index 000000000..3f5b25690
--- /dev/null
+++ b/spec/system/claims/payments/claims/download_claims_spec.rb
@@ -0,0 +1,62 @@
+require "rails_helper"
+
+RSpec.describe "Download payment claims", service: :claims, type: :system do
+ let(:payment) { create(:claims_payment) }
+
+ let(:valid_token) { Rails.application.message_verifier(:payments).generate(payment.id, expires_in: 7.days) }
+ let(:invalid_token) { Rails.application.message_verifier(:payments).generate("invalid_id", expires_in: 7.days) }
+ let(:expired_token) { Rails.application.message_verifier(:payments).generate(payment.id, expires_at: 10.days.ago) }
+ let(:random_token) { "random" }
+
+ scenario "ESFA user clicks on a valid link in their email inbox" do
+ when_i_visit_the_download_url(token: valid_token)
+ then_i_see_a_page_with_download_button
+
+ when_i_click_on_the_download_button
+ then_a_csv_file_is_downloaded
+ end
+
+ scenario "ESFA user clicks on an expired link in their email inbox" do
+ when_i_visit_the_download_url(token: expired_token)
+ then_i_see_an_error_page
+ end
+
+ scenario "ESFA user visits the url with an invalid token" do
+ when_i_visit_the_download_url(token: invalid_token)
+ then_i_see_an_error_page
+ end
+
+ scenario "ESFA user visits the url with a random token" do
+ when_i_visit_the_download_url(token: random_token)
+ then_i_see_an_error_page
+ end
+
+ scenario "ESFA user visits the url without a token" do
+ when_i_visit_the_download_url(token: nil)
+ then_i_see_an_error_page
+ end
+
+ private
+
+ def when_i_visit_the_download_url(token:)
+ visit claims_payments_claims_path(token:)
+ end
+
+ def then_i_see_a_page_with_download_button
+ expect(page).to have_css("h1.govuk-heading-l", text: "Download payments file")
+ end
+
+ def then_i_see_an_error_page
+ expect(page).to have_css("h1.govuk-heading-l", text: "Sorry, there is a problem with the download link")
+ expect(page).to have_css("p.govuk-body", text: "You are seeing this page because the download link is not working. It may have timed out or contained an invalid security token.")
+ expect(page).to have_css("p.govuk-body", text: "Email ittmentor.funding@education.gov.uk to request a new download link.")
+ end
+
+ def when_i_click_on_the_download_button
+ click_on "Download CSV file"
+ end
+
+ def then_a_csv_file_is_downloaded
+ expect(response_headers["Content-Type"]).to eq "text/csv"
+ end
+end
diff --git a/spec/system/claims/support/claims/payments/send_claims_to_esfa_spec.rb b/spec/system/claims/support/claims/payments/send_claims_to_esfa_spec.rb
new file mode 100644
index 000000000..8d6b88d09
--- /dev/null
+++ b/spec/system/claims/support/claims/payments/send_claims_to_esfa_spec.rb
@@ -0,0 +1,80 @@
+require "rails_helper"
+
+RSpec.describe "Send claims to ESFA", service: :claims, type: :system do
+ let(:support_user) { create(:claims_support_user) }
+
+ before do
+ user_exists_in_dfe_sign_in(user: support_user)
+ end
+
+ scenario "Support user attempts to send claims to ESFA, but there are no 'submitted' claims" do
+ given_i_sign_in
+ when_i_visit_claims_payments_index_page
+ when_i_click_on_send_claims_to_esfa
+ then_i_can_see_an_error_page
+ end
+
+ context "when there are submitted claims" do
+ before do
+ create_list(:claim, 3, :submitted)
+ end
+
+ scenario "Support user sends claims to ESFA" do
+ given_i_sign_in
+ when_i_visit_claims_payments_index_page
+ when_i_click_on_send_claims_to_esfa
+ then_i_can_see_a_confirmation_page
+
+ when_i_click_on_send_claims
+ then_i_see_a_success_flash_message
+ and_claims_have_been_sent_to_esfa
+ end
+ end
+
+ private
+
+ def given_i_sign_in
+ visit sign_in_path
+ click_on "Sign in using DfE Sign In"
+ end
+
+ def when_i_visit_claims_payments_index_page
+ click_on("Claims")
+ click_on("Payments")
+ end
+
+ def when_i_click_on_send_claims_to_esfa
+ click_on("Send claims to ESFA")
+ 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: "There are no claims to send for payment")
+ expect(page).to have_css("p.govuk-body", text: "You cannot send any claims to the ESFA because there are no claims pending payment.")
+ expect(page).to have_link("Cancel", href: claims_support_claims_payments_path)
+ end
+
+ 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: "Send claims to ESFA")
+ expect(page).to have_css("p.govuk-body", text: "There are 3 claims included in this submission.")
+ expect(page).to have_css("p.govuk-body", text: "Selecting ‘Send claims’ will:")
+ expect(page).to have_css("li", text: "create a CSV containing a list of all ‘Submitted’ claims")
+ expect(page).to have_css("li", text: "send an email to the ESFA containing a link to the generated CSV - this link expires after 7 days")
+ expect(page).to have_css("li", text: "update the claim status from ‘Submitted’ to ‘Payment in progress’")
+ expect(page).to have_css("div.govuk-warning-text", text: "This action cannot be undone.")
+ end
+
+ def when_i_click_on_send_claims
+ click_on("Send claims")
+ end
+
+ def then_i_see_a_success_flash_message
+ expect(page).to have_content("Claims sent to ESFA")
+ end
+
+ def and_claims_have_been_sent_to_esfa
+ expect(Claims::Claim.submitted.count).to eq(0)
+ expect(Claims::Claim.payment_in_progress.count).to eq(3)
+ end
+end