feat(procedure): send notifications after closing

This commit is contained in:
Eric Leroy-Terquem 2024-02-15 11:09:00 +01:00
parent c147d9b36c
commit c95f0f1cad
13 changed files with 307 additions and 11 deletions

View file

@ -212,6 +212,38 @@ module Administrateurs
redirect_to admin_procedures_path
end
def closing_notification
@procedure = current_administrateur.procedures.find(params[:procedure_id])
@users_brouillon_count = @procedure.dossiers.not_archived.state_brouillon.count('distinct user_id')
@users_en_cours_count = @procedure.dossiers.not_archived.state_en_construction_ou_instruction.count('distinct user_id')
end
def notify_after_closing
@procedure = current_administrateur.procedures.find(params[:procedure_id])
@procedure.update!(notification_closing_params)
if (@procedure.closing_notification_brouillon? && params[:email_content_brouillon].blank?) || (@procedure.closing_notification_en_cours? && params[:email_content_en_cours].blank?)
flash.alert = "Veuillez renseigner le contenu de lemail afin dinformer les usagers"
redirect_to admin_procedure_closing_notification_path and return
end
if @procedure.closing_notification_brouillon?
user_ids = @procedure.dossiers.not_archived.state_brouillon.pluck(:user_id).uniq
content = params[:email_content_brouillon]
SendClosingNotificationJob.perform_later(user_ids, content, @procedure)
flash.notice = "Les emails sont en cours d'envoi"
end
if @procedure.closing_notification_en_cours?
user_ids = @procedure.dossiers.not_archived.state_en_construction_ou_instruction.pluck(:user_id).uniq
content = params[:email_content_en_cours]
SendClosingNotificationJob.perform_later(user_ids, content, @procedure)
flash.notice = "Les emails sont en cours denvoi"
end
redirect_to admin_procedures_path
end
def destroy
procedure = current_administrateur.procedures.find(params[:id])
@ -304,6 +336,10 @@ module Administrateurs
.update!(replaced_by_procedure: @procedure)
end
# TO DO after data backfill add this condition before reset :
# if @procedure.closing_reason.present?
@procedure.reset_closing_params
redirect_to admin_procedure_confirmation_path(@procedure)
rescue ActiveRecord::RecordInvalid
flash.alert = @procedure.errors.full_messages
@ -509,6 +545,10 @@ module Administrateurs
closing_params
end
def notification_closing_params
params.require(:procedure).permit(:closing_notification_brouillon, :closing_notification_en_cours)
end
def allow_decision_access_params
params.require(:experts_procedure).permit(:allow_decision_access)
end

View file

@ -0,0 +1,76 @@
import { ApplicationController } from './application_controller';
import { hide, show } from '@utils';
export class ClosingNotificationController extends ApplicationController {
static targets = [
'brouillonToggle',
'emailContentBrouillon',
'enCoursToggle',
'emailContentEnCours',
'submit'
];
declare readonly brouillonToggleTarget: HTMLInputElement;
declare readonly hasBrouillonToggleTarget: boolean;
declare readonly enCoursToggleTarget: HTMLInputElement;
declare readonly hasEnCoursToggleTarget: boolean;
declare readonly emailContentBrouillonTarget: HTMLElement;
declare readonly emailContentEnCoursTarget: HTMLElement;
declare readonly submitTarget: HTMLButtonElement;
connect() {
this.displayBrouillonInput();
this.displayEnCoursInput();
this.on('change', () => this.onChange());
}
onChange() {
this.displayBrouillonInput();
this.displayEnCoursInput();
}
displayBrouillonInput() {
if (this.hasBrouillonToggleTarget) {
const brouillonToggleElement = this
.brouillonToggleTarget as HTMLInputElement;
const emailContentBrouillonElement = this
.emailContentBrouillonTarget as HTMLElement;
if (emailContentBrouillonElement) {
if (brouillonToggleElement.checked) {
show(emailContentBrouillonElement);
} else {
hide(emailContentBrouillonElement);
}
}
}
}
displayEnCoursInput() {
if (this.hasEnCoursToggleTarget) {
const enCoursToggleElement = this.enCoursToggleTarget as HTMLInputElement;
const emailContentEnCoursElement = this
.emailContentEnCoursTarget as HTMLElement;
if (emailContentEnCoursElement) {
if (enCoursToggleElement.checked) {
show(this.emailContentEnCoursTarget);
} else {
hide(this.emailContentEnCoursTarget);
}
}
}
}
enableSubmitOnClick() {
if (
this.element.querySelectorAll('input[type="checkbox"]:checked').length > 0
) {
this.submitTarget.disabled = false;
} else {
this.submitTarget.disabled = true;
}
}
}

View file

@ -0,0 +1,7 @@
class SendClosingNotificationJob < ApplicationJob
def perform(user_ids, content, procedure)
User.where(id: user_ids).find_each do |user|
Expired::MailRateLimiter.new().send_with_delay(UserMailer.notify_after_closing(user, content, @procedure))
end
end
end

View file

@ -74,6 +74,15 @@ class UserMailer < ApplicationMailer
mail(to: user.email, subject: @subject)
end
def notify_after_closing(user, content, procedure = nil)
@user = user
@subject = "Clôture d'une démarche sur Démarches simplifiées"
@procedure = procedure
@content = content
mail(to: user.email, subject: @subject, content: @content, procedure: @procedure)
end
def self.critical_email?(action_name)
[
'france_connect_merge_confirmation',

View file

@ -1010,6 +1010,10 @@ class Procedure < ApplicationRecord
.first
end
def reset_closing_params
update!(closing_reason: nil, closing_details: nil, replaced_by_procedure_id: nil, closing_notification_brouillon: false, closing_notification_en_cours: false)
end
private
def pieces_jointes_list

View file

@ -0,0 +1,52 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],[t('administrateurs.procedures.close.page_title')]],
metadatas: true }
.fr-container
.fr-grid-row
.fr-col-12.fr-col-offset-md-2.fr-col-md-8
%h1= t('administrateurs.procedures.closing_notification.page_title')
- if @procedure.closing_reason == Procedure.closing_reasons.fetch(:other)
%h2.fr-h5= I18n.t('administrateurs.procedures.closing_notification.page_subtitle', closing_path: closing_details_path(@procedure.path)).html_safe
- else
%h2.fr-h5= I18n.t('administrateurs.procedures.closing_notification.page_subtitle_with_redirection', redirection_path: commencer_path(@procedure.replaced_by_procedure.path)).html_safe
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do
%p
= t('administrateurs.procedures.closing_notification.callout_content')
= form_for @procedure,
url: admin_procedure_notify_after_closing_path(@procedure),
method: :post,
html: { "data-controller" => "closing-notification" } do |f|
%div{ data: { 'action': "click->closing-notification#enableSubmitOnClick" } }
- if @users_brouillon_count != 0
= render Dsfr::ToggleComponent.new(form: f,
target: :closing_notification_brouillon,
title: t("administrateurs.procedures.closing_notification.email_toggle_brouillon", count: @users_brouillon_count),
toggle_labels: {checked: 'Oui', unchecked: 'Non'},
opt: {"closing-notification-target" => "brouillonToggle"})
.fr-input-group{ "data-closing-notification-target" => "emailContentBrouillon" }
= label_tag :email_content_brouillon, t("administrateurs.procedures.closing_notification.email_content_brouillon"), class: "fr-label"
= text_area_tag :email_content_brouillon, '', class: "fr-input"
- if @users_en_cours_count != 0
= render Dsfr::ToggleComponent.new(form: f,
target: :closing_notification_en_cours,
title: t("administrateurs.procedures.closing_notification.email_toggle_en_cours", count: @users_en_cours_count),
toggle_labels: {checked: 'Oui', unchecked: 'Non'},
opt: {"closing-notification-target" => "enCoursToggle"})
.fr-input-group{ "data-closing-notification-target" => "emailContentEnCours" }
= label_tag :email_content_en_cours, t("administrateurs.procedures.closing_notification.email_content_en_cours"), class: "fr-label"
= text_area_tag :email_content_en_cours, '', class: "fr-input"
%ul.fr-btns-group.fr-btns-group--inline-md
%li
= submit_tag t('administrateurs.procedures.close.actions.notify_after_closing'), { class: "fr-btn", id: 'publish', disabled: true, data: { confirm: "Vous allez informer les usagers de la clôture de la démarche. Souhaitez-vous continuer ?", disable_with: "Envoi des notifications…", 'closing-notification-target': 'submit'} }
%li
= link_to t('administrateurs.procedures.close.actions.cancel'), admin_procedures_path, class: 'fr-btn fr-btn--secondary fr-ml-2w'

View file

@ -0,0 +1,4 @@
- content_for(:title, @subject)
%p
= simple_format(@content)

View file

@ -6,7 +6,22 @@ en:
replacement_procedure_callout_title: You are about to close a procedure
replacement_procedure_callout_content: Files « in construction » or « instructing » can be instructed, but no new files can be filed.
actions:
close_procedure: Close the procedure
close_procedure: Close procedure
notify_after_closing: Notify users
cancel: Cancel
closing_notification:
page_title: Alert users
page_subtitle: Your procedure has been successfully closed. The link of the procedure now redirects to this <a href="%{closing_path}" target="_blank" rel="noopener noreferrer">closing page</a>.
page_subtitle_with_redirection: Your procedure has been successfully closed. The link of the procedure now redirects to this <a href="%{redirection_path}" target="_blank" rel="noopener noreferrer">procedure</a>.
callout_content: You can continue to examine the submitted files. If you do not intend to examine these files, we invite you to inform users
email_toggle_brouillon:
one: You want to send an email to the user with a draft folder
other: You want to send an email to %{count} users with a draft folder
email_content_brouillon: You want to send an email to users with a draft file
email_toggle_en_cours:
one: You want to send an email to the user with a submitted file
other: You want to send an email to %{count} users with a submitted file
email_content_en_cours: You want to send an email to users with a file « in construction » or « instructing »
preview_unavailable: Preview is unavailable due to procedure misconfiguration
modifications:
dossiers_en_construction_and_dossiers_en_instruction: "%{en_construction_count} files « in construction » and %{en_instruction_count} files « instructing » on this procedure version."

View file

@ -7,6 +7,21 @@ fr:
replacement_procedure_callout_content: Les dossiers en cours pourront être instruits, mais aucun nouveau dossier ne pourra plus être déposé.
actions:
close_procedure: Clore la démarche
notify_after_closing: Informer les usagers
cancel: Retour
closing_notification:
page_title: Alerter les usagers
page_subtitle: Votre démarche est close. Le lien de votre démarche redirige désormais vers cette <a href="%{closing_path}" target="_blank" rel="noopener noreferrer">page de fermeture</a>.
page_subtitle_with_redirection: Votre démarche est close. Le lien public redirige désormais vers cette <a href="%{redirection_path}" target="_blank" rel="noopener noreferrer">démarche</a>.
callout_content: Vous avez la possibilité de continuer à instruire les dossiers déposés. Si vous navez pas lintention dinstruire ces dossiers, nous vous invitons à en informer les usagers.
email_toggle_brouillon:
one: Souhaitez-vous envoyer un email à l'utilisateur avec un dossier en brouillon ?
other: Souhaitez-vous envoyer un email aux %{count} utilisateurs avec un dossier en brouillon ?
email_content_brouillon: Contenu de l'email
email_toggle_en_cours:
one : Souhaitez-vous envoyer un email à l'utilisateur avec un dossier déposé ?
other: Souhaitez-vous envoyer un email aux %{count} utilisateurs avec un dossier déposé ?
email_content_en_cours: Contenu de l'email
preview_unavailable: Aperçu non disponible car la démarche est mal configurée
modifications:
dossiers_en_construction_and_dossiers_en_instruction: Il y a %{en_construction_count} dossiers « en construction » et %{en_instruction_count} dossiers « en instruction » sur cette version de la démarche.

View file

@ -600,6 +600,8 @@ Rails.application.routes.draw do
put 'publish_revision' => 'procedures#publish_revision', as: :publish_revision
get 'transfert' => 'procedures#transfert', as: :transfert
get 'close' => 'procedures#close', as: :close
get 'closing_notification' => 'procedures#closing_notification', as: :closing_notification
post 'notify_after_closing' => 'procedures#notify_after_closing', as: :notify_after_closing
get 'confirmation' => 'procedures#confirmation', as: :confirmation
post 'transfer' => 'procedures#transfer', as: :transfer
resources :mail_templates, only: [:edit, :update, :show]

View file

@ -696,7 +696,7 @@ describe Administrateurs::ProceduresController, type: :controller do
it 'archives the procedure' do
expect(procedure.close?).to be_truthy
expect(response).to redirect_to :admin_procedures
expect(response).to redirect_to admin_procedure_path(procedure.id)
expect(flash[:notice]).to have_content 'Démarche close'
end
@ -724,8 +724,7 @@ describe Administrateurs::ProceduresController, type: :controller do
it 'archives the procedure' do
expect(procedure.close?).to be_truthy
expect(response).to redirect_to :admin_procedures
expect(flash[:notice]).to have_content 'Démarche close'
expect(response).to redirect_to admin_procedure_closing_notification_path
end
it 'does have a replacement procedure' do
@ -743,15 +742,9 @@ describe Administrateurs::ProceduresController, type: :controller do
it 'archives the procedure' do
expect(procedure.close?).to be_truthy
expect(response).to redirect_to :admin_procedures
expect(response).to redirect_to admin_procedure_path(procedure.id)
expect(flash[:notice]).to have_content 'Démarche close'
end
it 'does have a replacement procedure' do
expect(procedure.replaced_by_procedure).to eq(nil)
expect(procedure.replaced_by_external_url).to eq('new_url.com')
expect(procedure.closing_reason).to eq('external_procedure')
end
end
context 'when the admin is not an owner of the procedure' do
@ -770,6 +763,42 @@ describe Administrateurs::ProceduresController, type: :controller do
expect(flash[:alert]).to have_content 'Démarche inexistante'
end
end
context 'when the admin is not an owner of the new procedure in DS' do
let(:admin_2) { create(:administrateur) }
let(:other_admin_procedure) { create(:procedure, :with_all_champs, administrateurs: [admin_2]) }
before do
put :archive, params: { procedure_id: procedure.id, procedure: { closing_reason: 'internal_procedure', replaced_by_procedure_id: other_admin_procedure.id } }
procedure.reload
end
it 'closes the procedure without redirection to the new procedure in DS' do
expect(response).to redirect_to admin_procedure_path(procedure.id)
expect(flash[:notice]).to have_content 'Démarche close'
expect(procedure.replaced_by_procedure).to eq(nil)
end
end
end
describe 'POST #notify_after_closing' do
let(:procedure_closed) { create(:procedure_with_dossiers, :closed, administrateurs: [admin]) }
let(:user_ids) { [procedure_closed.dossiers.first.user.id] }
let(:email_content) { "La démarche a fermé" }
subject do
post :notify_after_closing, params: { procedure_id: procedure_closed.id, procedure: { closing_notification_brouillon: true }, email_content_brouillon: email_content }
end
before do
sign_in(admin.user)
end
it 'redirects to admin procedures' do
expect { subject }.to have_enqueued_job(SendClosingNotificationJob).with(user_ids, email_content, procedure_closed)
expect(flash.notice).to eq("Les emails sont en cours d'envoi")
expect(response).to redirect_to :admin_procedures
end
end
describe 'DELETE #destroy' do
@ -1056,6 +1085,27 @@ describe Administrateurs::ProceduresController, type: :controller do
end
end
context 'procedure was closed and is re opened' do
before do
procedure.publish!
procedure.update!(closing_reason: 'internal_procedure', replaced_by_procedure_id: procedure2.id)
procedure.close!
procedure.update!(closing_notification_brouillon: true, closing_notification_en_cours: true)
perform_request
procedure.reload
procedure2.reload
end
it 'publish the given procedure and reset closing params' do
expect(procedure.publiee?).to be_truthy
expect(procedure.path).to eq(path)
expect(procedure.closing_reason).to be_nil
expect(procedure.replaced_by_procedure_id).to be_nil
expect(procedure.closing_notification_brouillon).to be_falsy
expect(procedure.closing_notification_en_cours).to be_falsy
end
end
context 'procedure path exists and is not owned by current administrator' do
let(:path) { procedure3.path }
let(:lien_site_web) { 'http://mon-site.gouv.fr' }

View file

@ -33,6 +33,10 @@ class UserMailerPreview < ActionMailer::Preview
UserMailer.notify_inactive_close_to_deletion(user)
end
def notify_after_closing
UserMailer.notify_after_closing([user])
end
private
def user

View file

@ -127,4 +127,22 @@ RSpec.describe UserMailer, type: :mailer do
end
end
end
describe '.notify_after_closing' do
let(:procedure) { create(:procedure) }
let(:content) { "Bonjour,\r\nsaut de ligne" }
subject { described_class.notify_after_closing(user, content, procedure) }
it { expect(subject.to).to eq([user.email]) }
it { expect(subject.body).to include("Clôture d&#39;une démarche sur Démarches simplifiées") }
it { expect(subject.body).to include("Bonjour,\r\n<br />saut de ligne") }
context 'when perform_later is called' do
let(:custom_queue) { 'low_priority' }
before { ENV['BULK_EMAIL_QUEUE'] = custom_queue }
it 'enqueues email is custom queue for low priority delivery' do
expect { subject.deliver_later }.to have_enqueued_job.on_queue(custom_queue)
end
end
end
end