Skip to content

Commit

Permalink
Merge pull request #3445 from DFE-Digital/unsubscribe
Browse files Browse the repository at this point in the history
[CAPT-1862] Add unsubscribe functionality
  • Loading branch information
asmega authored Jan 17, 2025
2 parents 5f90ec8 + b320903 commit 5bcdb52
Show file tree
Hide file tree
Showing 38 changed files with 428 additions and 137 deletions.
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: "",
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]

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

0 comments on commit 5bcdb52

Please sign in to comment.