Skip to content

[CAPT-1862] Add unsubscribe functionality #3445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/controllers/unsubscribe_controller.rb
Original file line number Diff line number Diff line change
@@ -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.mark_as_deleted!
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
6 changes: 5 additions & 1 deletion app/forms/reminders/email_verification_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def save!
journey_class: journey.to_s
)

reminder.undelete!

ReminderMailer.reminder_set(reminder).deliver_now
end

Expand All @@ -34,7 +36,9 @@ def set_reminder_from_claim
private

def itt_subject
journey_session.answers.eligible_itt_subject
if journey_session.answers.respond_to?(:eligible_itt_subject)
journey_session.answers.eligible_itt_subject
end
end

def next_academic_year
Expand Down
2 changes: 2 additions & 0 deletions app/forms/reminders/personal_details_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def set_reminder_from_claim
journey_class: journey.to_s
)

reminder.undelete!

ReminderMailer.reminder_set(reminder).deliver_now
else
false
Expand Down
30 changes: 30 additions & 0 deletions app/forms/unsubscribe/confirmation_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module Unsubscribe
class ConfirmationForm
include ActiveModel::Model
include ActiveModel::Attributes

attribute :id, :string

def reminder
@reminder ||= Reminder.not_deleted.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
default = I18n.t("journey_name", scope: reminder.journey::I18N_NAMESPACE).downcase
I18n.t("policy_short_name", scope: reminder.journey::I18N_NAMESPACE, default:).downcase
end
end
end
41 changes: 27 additions & 14 deletions app/mailers/reminder_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
4 changes: 4 additions & 0 deletions app/models/concerns/deletable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ def mark_as_deleted!

update!(deleted_at: Time.zone.now)
end

def undelete!
update!(deleted_at: nil)
end
end
6 changes: 6 additions & 0 deletions app/models/reminder.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Reminder < ApplicationRecord
include Deletable

attribute :sent_one_time_password_at, :datetime
attribute :one_time_password, :string, limit: 6

Expand All @@ -21,4 +23,8 @@ def itt_academic_year
read_attribute(:itt_academic_year)
)
end

def soft_delete!
update!(deleted_at: Time.now)
end
end
2 changes: 1 addition & 1 deletion app/views/reminders/confirmation.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<h2 class="govuk-heading-m">What happens next</h2>

<p class="govuk-body">
We will send you a verification email now. If you don’t receive one,
We will send you a confirmation email now. If you don’t receive one,
contact us on <%= mail_to t("support_email_address", scope: journey::I18N_NAMESPACE), t("support_email_address", scope: journey::I18N_NAMESPACE), class: "govuk-link" %>.
</p>

Expand Down
11 changes: 11 additions & 0 deletions app/views/unsubscribe/create.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%= content_for(:page_title) { "Unsubscribe" } %>

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= govuk_panel(title_text: "Unsubscribe complete") %>

<p class="govuk-body">
You will no longer receive your <%= @form.journey_name %> reminder to <%= @form.obfuscasted_email %>.
</p>
</div>
</div>
31 changes: 31 additions & 0 deletions app/views/unsubscribe/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<%= content_for(:page_title) { "Unsubscribe" } %>

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form,
url: unsubscribe_index_path,
scope: "",
Copy link
Contributor Author

@asmega asmega Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • empty scope is used here so form does not use nested parameters
  • this means the structure of the data supports a simpler format allowing the same controller endpoint to support one click unsubscriptions

builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %>

<%= f.hidden_field :id %>

<h1 class="govuk-heading-l">
Are you sure you wish to unsubscribe from your reminder?
</h1>

<p class="govuk-body">
You will no longer receive reminders regarding future application windows for targeted retention incentive payments from the Department for Education.
<p>

<p class="govuk-body">
However, if you have already submitted a claim for a targeted retention incentive payment, you will still continue to receive updates about the progress of your claim.
<p>

<p class="govuk-body">
You may also continue to receive communications related to the Claim Additional Payments service. Please email <%= govuk_mail_to I18n.t(:support_email_address) %> if you no longer want to receive emails.
<p>

<%= f.govuk_submit "Unsubscribe" %>
<% end %>
</div>
</div>
15 changes: 15 additions & 0 deletions app/views/unsubscribe/subscription_not_found.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-l">
We can’t find your subscription
</h1>

<p class="govuk-body">
You may have already unsubscribed. If you haven’t, check your email and copy the link again.
</p>

<p class="govuk-body">
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]) %>
</p>
</div>
</div>
1 change: 1 addition & 0 deletions config/analytics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ shared:
- itt_subject
- sent_one_time_password_at
- journey_class
- deleted_at
:tasks:
- id
- name
Expand Down
3 changes: 1 addition & 2 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ en:
format:
unit: "£"
service_name: "Claim additional payments for teaching"
support_email_address: "additionalteachingpayment@digital.education.gov.uk"
support_email_address: "additional.teachingpayment@education.gov.uk"
check_your_answers:
heading_send_application: Confirm and send your application
statement:
Expand Down
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ def matches?(request)
end
end

resources :unsubscribe,
path: "/unsubscribe/:resource_class",
constraints: {resource_class: /reminders/},
only: [:create, :show]

Comment on lines +68 to +72
Copy link
Contributor Author

@asmega asmega Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resource_class is not currently used. but has been added so can be extended to support unsubscribing of other resource in the future easily if needed

scope constraints: {journey: /further-education-payments|additional-payments/} do
resources :reminders,
only: [:show, :update],
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20241206105631_add_deleted_at_to_reminders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddDeletedAtToReminders < ActiveRecord::Migration[8.0]
def change
add_column :reminders, :deleted_at, :datetime
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2024_12_05_121421) do
ActiveRecord::Schema[8.0].define(version: 2024_12_06_105631) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_catalog.plpgsql"
Expand Down Expand Up @@ -429,6 +429,7 @@
t.string "itt_academic_year", limit: 9
t.string "itt_subject"
t.text "journey_class", null: false
t.datetime "deleted_at"
t.index ["journey_class"], name: "index_reminders_on_journey_class"
end

Expand Down
4 changes: 4 additions & 0 deletions spec/factories/reminders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
trait :with_fe_reminder do
journey_class { Journeys::FurtherEducationPayments.to_s }
end

trait :soft_deleted do
deleted_at { 1.second.ago }
end
end
end
41 changes: 41 additions & 0 deletions spec/features/unsubscribe_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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"

click_button("Unsubscribe")

expect(reminder.reload.deleted_at).to be_present

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

context "when reminder already soft deleted" do
let(:reminder) do
create(
:reminder,
:soft_deleted,
journey_class: Journeys::FurtherEducationPayments.to_s
)
end

scenario "cannot find subscription" do
visit "/#{reminder.journey::ROUTING_NAME}/unsubscribe/reminders/#{reminder.id}"
expect(page).to have_content "We can’t find your subscription"
end
end
end
8 changes: 0 additions & 8 deletions spec/forms/current_school_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}}) }
Expand Down
Loading
Loading