diff --git a/app/controllers/unsubscribe_controller.rb b/app/controllers/unsubscribe_controller.rb
new file mode 100644
index 0000000000..b0f7ae2efc
--- /dev/null
+++ b/app/controllers/unsubscribe_controller.rb
@@ -0,0 +1,37 @@
+class UnsubscribeController < ApplicationController
+ skip_forgery_protection
+
+ def show
+ @form = Unsubscribe::ConfirmationForm.new(form_params)
+
+ if @form.reminder.nil?
+ render "subscription_not_found"
+ end
+ end
+
+ def create
+ @form = Unsubscribe::ConfirmationForm.new(form_params)
+
+ if @form.reminder.nil?
+ head :bad_request
+ else
+ @form.reminder.destroy
+ end
+ end
+
+ private
+
+ def form_params
+ params.permit([:id])
+ end
+
+ def current_journey_routing_name
+ params[:journey]
+ end
+ helper_method :current_journey_routing_name
+
+ def current_journey
+ Journeys.for_routing_name(params[:journey])
+ end
+ helper_method :current_journey
+end
diff --git a/app/forms/unsubscribe/confirmation_form.rb b/app/forms/unsubscribe/confirmation_form.rb
new file mode 100644
index 0000000000..726180ac3c
--- /dev/null
+++ b/app/forms/unsubscribe/confirmation_form.rb
@@ -0,0 +1,29 @@
+module Unsubscribe
+ class ConfirmationForm
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+
+ attribute :id, :string
+
+ def reminder
+ @reminder ||= Reminder.find_by(id:)
+ end
+
+ def obfuscasted_email
+ head, tail = reminder.email_address.split("@")
+
+ mask = case head.size
+ when 1, 2, 3
+ "*" * head.size
+ else
+ [head[0], "*" * (head.length - 2), head[-1]].join
+ end
+
+ [mask, "@", tail].join
+ end
+
+ def journey_name
+ I18n.t("journey_name", scope: reminder.journey::I18N_NAMESPACE).downcase
+ end
+ end
+end
diff --git a/app/mailers/reminder_mailer.rb b/app/mailers/reminder_mailer.rb
index e48a712a07..54313f5cc6 100644
--- a/app/mailers/reminder_mailer.rb
+++ b/app/mailers/reminder_mailer.rb
@@ -19,7 +19,13 @@ def email_verification(reminder, one_time_password, journey_name)
journey_name:
}
- send_mail(OTP_EMAIL_NOTIFY_TEMPLATE_ID, personalisation)
+ template_mail(
+ OTP_EMAIL_NOTIFY_TEMPLATE_ID,
+ to: @reminder.email_address,
+ reply_to_id: GENERIC_NOTIFY_REPLY_TO_ID,
+ subject: @subject,
+ personalisation:
+ )
end
def reminder_set(reminder)
@@ -29,10 +35,17 @@ def reminder_set(reminder)
personalisation = {
first_name: extract_first_name(@reminder.full_name),
support_email_address: support_email_address,
- next_application_window: @reminder.send_year
+ next_application_window: @reminder.send_year,
+ unsubscribe_url: unsubscribe_url(reminder:)
}
- send_mail(REMINDER_SET_NOTIFY_TEMPLATE_ID, personalisation)
+ template_mail(
+ REMINDER_SET_NOTIFY_TEMPLATE_ID,
+ to: @reminder.email_address,
+ reply_to_id: GENERIC_NOTIFY_REPLY_TO_ID,
+ subject: @subject,
+ personalisation:
+ )
end
# TODO: This template only accommodates LUP/ECP claims currently. Needs to
@@ -47,22 +60,22 @@ def reminder(reminder)
support_email_address: support_email_address
}
- send_mail(REMINDER_APPLICATION_WINDOW_OPEN_NOTIFY_TEMPLATE_ID, personalisation)
- end
-
- private
-
- def extract_first_name(fullname)
- (fullname || "").split(" ").first
- end
-
- def send_mail(template_id, personalisation)
template_mail(
- template_id,
+ REMINDER_APPLICATION_WINDOW_OPEN_NOTIFY_TEMPLATE_ID,
to: @reminder.email_address,
reply_to_id: GENERIC_NOTIFY_REPLY_TO_ID,
subject: @subject,
personalisation:
)
end
+
+ private
+
+ def unsubscribe_url(reminder:)
+ "https://#{ENV["CANONICAL_HOSTNAME"]}/#{reminder.journey::ROUTING_NAME}/unsubscribe/reminders/#{reminder.id}"
+ end
+
+ def extract_first_name(fullname)
+ (fullname || "").split(" ").first
+ end
end
diff --git a/app/views/unsubscribe/create.html.erb b/app/views/unsubscribe/create.html.erb
new file mode 100644
index 0000000000..4a138d01e6
--- /dev/null
+++ b/app/views/unsubscribe/create.html.erb
@@ -0,0 +1,11 @@
+<%= content_for(:page_title) { "Unsubscribe" } %>
+
+
+
+ <%= govuk_panel(title_text: "Unsubscribe complete") %>
+
+
+ You will no longer receive your <%= @form.journey_name %> reminder to <%= @form.obfuscasted_email %>.
+
+
+
diff --git a/app/views/unsubscribe/show.html.erb b/app/views/unsubscribe/show.html.erb
new file mode 100644
index 0000000000..4011922864
--- /dev/null
+++ b/app/views/unsubscribe/show.html.erb
@@ -0,0 +1,27 @@
+<%= content_for(:page_title) { "Unsubscribe" } %>
+
+
+
+ <%= form_with model: @form,
+ url: unsubscribe_index_path,
+ scope: "",
+ builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %>
+
+ <%= f.hidden_field :id %>
+
+
+ Are you sure you wish to unsubscribe from your <%= @form.journey_name %> reminder?
+
+
+
+ You will no longer receive your reminder to <%= @form.obfuscasted_email %>.
+
+
+
+ However, you will still continue to receive updates about your claim as it progresses.
+
+
+ <%= f.govuk_submit "Unsubscribe" %>
+ <% end %>
+
+
diff --git a/app/views/unsubscribe/subscription_not_found.html.erb b/app/views/unsubscribe/subscription_not_found.html.erb
new file mode 100644
index 0000000000..dc26268a90
--- /dev/null
+++ b/app/views/unsubscribe/subscription_not_found.html.erb
@@ -0,0 +1,15 @@
+
+
+
+ We can’t find your subscription
+
+
+
+ You may have already unsubscribed. If you haven’t, check your email and copy the link again.
+
+
+
+ If you need more help, contact support at <%= govuk_mail_to I18n.t("support_email_address", scope: [current_journey::I18N_NAMESPACE]), I18n.t("support_email_address", scope: [current_journey::I18N_NAMESPACE]) %>
+
+
+
diff --git a/config/environments/development.rb b/config/environments/development.rb
index d7b3cb6f23..c656970451 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -60,8 +60,7 @@
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
- # Raise an exception if parameters that are not explicitly permitted are found
- config.action_controller.action_on_unpermitted_parameters = :raise
+ config.action_controller.action_on_unpermitted_parameters = :log
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
diff --git a/config/environments/test.rb b/config/environments/test.rb
index a92b501287..1601e36b07 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -33,8 +33,7 @@
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
- # Raise an exception if parameters that are not explicitly permitted are found
- config.action_controller.action_on_unpermitted_parameters = :raise
+ config.action_controller.action_on_unpermitted_parameters = :log
config.action_mailer.perform_caching = false
diff --git a/config/routes.rb b/config/routes.rb
index 3d62b88694..732803e7f7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -65,6 +65,11 @@ def matches?(request)
end
end
+ resources :unsubscribe,
+ path: "/unsubscribe/:resource_class",
+ constraints: {resource_class: /reminders/},
+ only: [:create, :show]
+
scope constraints: {journey: /further-education-payments|additional-payments/} do
resources :reminders,
only: [:show, :update],
diff --git a/spec/features/unsubscribe_spec.rb b/spec/features/unsubscribe_spec.rb
new file mode 100644
index 0000000000..7b7e742af6
--- /dev/null
+++ b/spec/features/unsubscribe_spec.rb
@@ -0,0 +1,26 @@
+require "rails_helper"
+
+RSpec.feature "unsubscribe from reminders" do
+ let(:reminder) do
+ create(
+ :reminder,
+ journey_class: Journeys::FurtherEducationPayments.to_s
+ )
+ end
+
+ scenario "happy path" do
+ visit "/#{reminder.journey::ROUTING_NAME}/unsubscribe/reminders/#{reminder.id}"
+ expect(page).to have_content "Are you sure you wish to unsub"
+
+ expect {
+ click_button("Unsubscribe")
+ }.to change(Reminder, :count).by(-1)
+
+ expect(page).to have_content "Unsubscribe complete"
+ end
+
+ scenario "when reminder does not exist" do
+ visit "/#{reminder.journey::ROUTING_NAME}/unsubscribe/reminders/idonotexist"
+ expect(page).to have_content "We can’t find your subscription"
+ end
+end
diff --git a/spec/forms/current_school_form_spec.rb b/spec/forms/current_school_form_spec.rb
index ac9afa847e..b68836251f 100644
--- a/spec/forms/current_school_form_spec.rb
+++ b/spec/forms/current_school_form_spec.rb
@@ -19,14 +19,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#schools" do
context "new form" do
let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) }
diff --git a/spec/forms/employed_directly_form_spec.rb b/spec/forms/employed_directly_form_spec.rb
index 6ecda4cf73..55cf332164 100644
--- a/spec/forms/employed_directly_form_spec.rb
+++ b/spec/forms/employed_directly_form_spec.rb
@@ -17,14 +17,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug:, claim: {random_param: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#employed_directly" do
let(:params) { ActionController::Parameters.new({slug:, claim: {}}) }
diff --git a/spec/forms/form_spec.rb b/spec/forms/form_spec.rb
index ebfa528cd0..6df0bb5002 100644
--- a/spec/forms/form_spec.rb
+++ b/spec/forms/form_spec.rb
@@ -48,14 +48,6 @@ def initialize(journey_session)
let(:claim_params) { {first_name: "test-name"} }
describe "#initialize" do
- context "with unpermitted params" do
- let(:claim_params) { {unpermitted: "my-name"} }
-
- it "raises an error" do
- expect { form }.to raise_error(ActionController::UnpermittedParameters)
- end
- end
-
context "with valid params" do
let(:claim_params) { {first_name: "my-name"} }
@@ -218,13 +210,5 @@ def initialize(journey_session)
expect(form.permitted_params).to eql(ActionController::Parameters.new(claim_params.stringify_keys).permit!)
end
end
-
- context "with params containing attributes not defined on the form" do
- let(:claim_params) { super().merge(unpermitted_attribute: "test-value") }
-
- it "raises an error" do
- expect { form.permitted_params }.to raise_error(ActionController::UnpermittedParameters)
- end
- end
end
end
diff --git a/spec/forms/gender_form_spec.rb b/spec/forms/gender_form_spec.rb
index 44b1587993..f31b88f025 100644
--- a/spec/forms/gender_form_spec.rb
+++ b/spec/forms/gender_form_spec.rb
@@ -25,14 +25,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#payroll_gender" do
let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) }
diff --git a/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb
index 6fd7e257a0..3c29ae7d14 100644
--- a/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb
+++ b/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb
@@ -17,14 +17,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug:, claim: {random_param: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#has_entire_term_contract" do
let(:params) { ActionController::Parameters.new({slug:, claim: {}}) }
diff --git a/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb
index 01f2e4bf20..285f522d93 100644
--- a/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb
+++ b/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb
@@ -17,14 +17,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#save" do
let(:params) { ActionController::Parameters.new({slug: slug, claim: {induction_completed: true}}) }
diff --git a/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb
index 7cbdd1c6d6..676285cbbe 100644
--- a/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb
+++ b/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb
@@ -31,14 +31,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#itt_academic_year" do
let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) }
diff --git a/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb b/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb
index 8a99a0d7b5..ed75fd423a 100644
--- a/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb
+++ b/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb
@@ -34,14 +34,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#schools" do
context "new form" do
let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) }
diff --git a/spec/forms/personal_details_form_spec.rb b/spec/forms/personal_details_form_spec.rb
index ad4a4c59cb..4aded18886 100644
--- a/spec/forms/personal_details_form_spec.rb
+++ b/spec/forms/personal_details_form_spec.rb
@@ -30,16 +30,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { {nonsense_id: 1} }
- let(:logged_in_with_tid) { nil }
- let(:teacher_id_user_info) { {} }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#show_name_section?" do
context "when not logged_in_with_tid" do
let(:logged_in_with_tid) { nil }
diff --git a/spec/forms/poor_performance_form_spec.rb b/spec/forms/poor_performance_form_spec.rb
index d4cf89040b..5f7d55b42d 100644
--- a/spec/forms/poor_performance_form_spec.rb
+++ b/spec/forms/poor_performance_form_spec.rb
@@ -11,14 +11,6 @@
it { expect(form).to be_a(Form) }
- context "with unpermitted params" do
- let(:claim_params) { {unpermitted_param: ""} }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "validations" do
context "subject_to_formal_performance_action" do
it "cannot be nil" do
diff --git a/spec/forms/provide_mobile_number_form_spec.rb b/spec/forms/provide_mobile_number_form_spec.rb
index 4b160b1cfd..02ceb4f9ca 100644
--- a/spec/forms/provide_mobile_number_form_spec.rb
+++ b/spec/forms/provide_mobile_number_form_spec.rb
@@ -29,16 +29,6 @@
)
end
- context "unpermitted claim param" do
- let(:provide_mobile_number) { nil }
-
- let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "validations" do
let(:provide_mobile_number) { nil }
diff --git a/spec/forms/supply_teacher_form_spec.rb b/spec/forms/supply_teacher_form_spec.rb
index 52d2f33ace..6969780ec7 100644
--- a/spec/forms/supply_teacher_form_spec.rb
+++ b/spec/forms/supply_teacher_form_spec.rb
@@ -17,14 +17,6 @@
)
end
- context "unpermitted claim param" do
- let(:params) { ActionController::Parameters.new({slug:, claim: {random_param: 1}}) }
-
- it "raises an error" do
- expect { form }.to raise_error ActionController::UnpermittedParameters
- end
- end
-
describe "#employed_as_supply_teacher" do
let(:params) { ActionController::Parameters.new({slug:, claim: {}}) }
diff --git a/spec/forms/unsubscribe/confirmation_form_spec.rb b/spec/forms/unsubscribe/confirmation_form_spec.rb
new file mode 100644
index 0000000000..9ae101be09
--- /dev/null
+++ b/spec/forms/unsubscribe/confirmation_form_spec.rb
@@ -0,0 +1,46 @@
+require "rails_helper"
+
+RSpec.describe Unsubscribe::ConfirmationForm do
+ describe "#obfuscasted_email" do
+ let(:reminder) {
+ create(
+ :reminder,
+ email_address:
+ )
+ }
+
+ subject { described_class.new(id: reminder.id) }
+
+ context "happy path" do
+ let(:email_address) { "hello@example.com" }
+
+ it "obfuscates" do
+ expect(subject.obfuscasted_email).to eql "h***o@example.com"
+ end
+ end
+
+ context "1 char email" do
+ let(:email_address) { "h@example.com" }
+
+ it "obfuscates" do
+ expect(subject.obfuscasted_email).to eql "*@example.com"
+ end
+ end
+
+ context "2 char email" do
+ let(:email_address) { "hh@example.com" }
+
+ it "obfuscates" do
+ expect(subject.obfuscasted_email).to eql "**@example.com"
+ end
+ end
+
+ context "3 char email" do
+ let(:email_address) { "hah@example.com" }
+
+ it "obfuscates" do
+ expect(subject.obfuscasted_email).to eql "***@example.com"
+ end
+ end
+ end
+end
diff --git a/spec/mailers/reminder_mailer_spec.rb b/spec/mailers/reminder_mailer_spec.rb
new file mode 100644
index 0000000000..ad94f31a3f
--- /dev/null
+++ b/spec/mailers/reminder_mailer_spec.rb
@@ -0,0 +1,15 @@
+require "rails_helper"
+
+RSpec.describe ReminderMailer do
+ let(:reminder) {
+ create(:reminder, :with_fe_reminder)
+ }
+
+ describe "#reminder_set" do
+ it "includes unsubscribe_url in personalisation" do
+ mail = described_class.reminder_set(reminder)
+
+ expect(mail.personalisation[:unsubscribe_url]).to eql("https://www.example.com/further-education-payments/unsubscribe/reminders/#{reminder.id}")
+ end
+ end
+end
diff --git a/spec/requests/admin/admin_amendments_spec.rb b/spec/requests/admin/admin_amendments_spec.rb
index ac416edf14..20c9868604 100644
--- a/spec/requests/admin/admin_amendments_spec.rb
+++ b/spec/requests/admin/admin_amendments_spec.rb
@@ -87,15 +87,11 @@
expect(response.body).to include("To amend the claim you must change at least one value")
end
- it "raises an error and does not create an amendment or update the claim when attempting to modify an attribute that isn't in the allowed list" do
+ it "does not create an amendment or update the claim when attempting to modify an attribute that isn't in the allowed list" do
original_counts = [claim.reference, claim.amendments.size]
- expect {
- post admin_claim_amendments_url(claim, amendment: {claim: {reference: "REF12345"},
- notes: "Claimant made a typo"})
- }.to raise_error(
- ActionController::UnpermittedParameters
- )
+ post admin_claim_amendments_url(claim, amendment: {claim: {reference: "REF12345"},
+ notes: "Claimant made a typo"})
expect([claim.reference, claim.amendments.size]).to eq(original_counts)
end
diff --git a/spec/requests/unsubscribe_spec.rb b/spec/requests/unsubscribe_spec.rb
new file mode 100644
index 0000000000..053e929d26
--- /dev/null
+++ b/spec/requests/unsubscribe_spec.rb
@@ -0,0 +1,25 @@
+require "rails_helper"
+
+RSpec.describe "unsubscribe", type: :request do
+ let!(:reminder) { create(:reminder, :with_fe_reminder) }
+
+ describe "POST #create" do
+ context "happy path" do
+ it "unsubscribes from reminder via one click unsubscribe" do
+ expect {
+ post "/further-education-payments/unsubscribe/reminders", params: {id: reminder.id}
+ }.to change(Reminder, :count).by(-1)
+
+ expect(response).to be_successful
+ end
+ end
+
+ context "when no such reminder" do
+ it "returns 400 error" do
+ post "/further-education-payments/unsubscribe/reminders", params: {id: "idonotexist"}
+
+ expect(response).to be_bad_request
+ end
+ end
+ end
+end