refactor(contact): suggest email correction, strict email validation, fix admin form
This commit is contained in:
parent
49be3a797a
commit
ff62e99e7b
15 changed files with 371 additions and 331 deletions
|
@ -2,11 +2,11 @@ class SupportController < ApplicationController
|
|||
invisible_captcha only: [:create], on_spam: :redirect_to_root
|
||||
|
||||
def index
|
||||
setup_context
|
||||
@form = Helpscout::Form.new(tags: tags_from_query_params, dossier_id: dossier&.id, current_user:)
|
||||
end
|
||||
|
||||
def admin
|
||||
setup_context_admin
|
||||
@form = Helpscout::Form.new(tags: tags_from_query_params, current_user:, for_admin: true)
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -17,85 +17,79 @@ class SupportController < ApplicationController
|
|||
return
|
||||
end
|
||||
|
||||
create_conversation_later
|
||||
@form = Helpscout::Form.new(support_form_params.except(:piece_jointe).merge(current_user:))
|
||||
|
||||
if @form.valid?
|
||||
create_conversation_later(@form)
|
||||
flash.notice = "Votre message a été envoyé."
|
||||
|
||||
if params[:admin]
|
||||
redirect_to root_path(formulaire_contact_admin_submitted: true)
|
||||
redirect_to root_path
|
||||
else
|
||||
redirect_to root_path(formulaire_contact_general_submitted: true)
|
||||
flash.alert = @form.errors.full_messages
|
||||
render @form.for_admin ? :admin : :index
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_context
|
||||
@dossier_id = dossier&.id
|
||||
@tags = tags
|
||||
@options = Helpscout::FormAdapter.options
|
||||
end
|
||||
|
||||
def setup_context_admin
|
||||
@tags = tags
|
||||
@options = Helpscout::FormAdapter.admin_options
|
||||
end
|
||||
|
||||
def create_conversation_later
|
||||
if params[:piece_jointe]
|
||||
def create_conversation_later(form)
|
||||
if support_form_params[:piece_jointe].present?
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: params[:piece_jointe].tempfile,
|
||||
filename: params[:piece_jointe].original_filename,
|
||||
content_type: params[:piece_jointe].content_type,
|
||||
io: support_form_params[:piece_jointe].tempfile,
|
||||
filename: support_form_params[:piece_jointe].original_filename,
|
||||
content_type: support_form_params[:piece_jointe].content_type,
|
||||
identify: false
|
||||
).tap(&:scan_for_virus_later)
|
||||
end
|
||||
|
||||
HelpscoutCreateConversationJob.perform_later(
|
||||
blob_id: blob&.id,
|
||||
subject: params[:subject],
|
||||
email: email,
|
||||
phone: params[:phone],
|
||||
text: params[:text],
|
||||
dossier_id: dossier&.id,
|
||||
subject: form.subject,
|
||||
email: current_user&.email || form.email,
|
||||
phone: form.phone,
|
||||
text: form.text,
|
||||
dossier_id: form.dossier_id,
|
||||
browser: browser_name,
|
||||
tags: tags
|
||||
tags: form.tags_array
|
||||
)
|
||||
end
|
||||
|
||||
def create_commentaire
|
||||
attributes = {
|
||||
piece_jointe: params[:piece_jointe],
|
||||
body: "[#{params[:subject]}]<br><br>#{params[:text]}"
|
||||
piece_jointe: support_form_params[:piece_jointe],
|
||||
body: "[#{support_form_params[:subject]}]<br><br>#{support_form_params[:text]}"
|
||||
}
|
||||
CommentaireService.create!(current_user, dossier, attributes)
|
||||
end
|
||||
|
||||
def tags
|
||||
[params[:tags], params[:type]].flatten.compact
|
||||
.map { |tag| tag.split(',') }
|
||||
.flatten
|
||||
.compact_blank.uniq
|
||||
end
|
||||
|
||||
def browser_name
|
||||
if browser.known?
|
||||
"#{browser.name} #{browser.version} (#{browser.platform.name})"
|
||||
end
|
||||
end
|
||||
|
||||
def tags_from_query_params
|
||||
support_form_params[:tags]&.join(",") || ""
|
||||
end
|
||||
|
||||
def direct_message?
|
||||
user_signed_in? && params[:type] == Helpscout::FormAdapter::TYPE_INSTRUCTION && dossier.present? && dossier.messagerie_available?
|
||||
user_signed_in? && support_form_params[:type] == Helpscout::Form::TYPE_INSTRUCTION && dossier.present? && dossier.messagerie_available?
|
||||
end
|
||||
|
||||
def dossier
|
||||
@dossier ||= current_user&.dossiers&.find_by(id: params[:dossier_id])
|
||||
end
|
||||
|
||||
def email
|
||||
current_user&.email || params[:email]
|
||||
@dossier ||= current_user&.dossiers&.find_by(id: support_form_params[:dossier_id])
|
||||
end
|
||||
|
||||
def redirect_to_root
|
||||
redirect_to root_path, alert: t('invisible_captcha.sentence_for_humans')
|
||||
end
|
||||
|
||||
def support_form_params
|
||||
keys = [:email, :subject, :text, :type, :dossier_id, :piece_jointe, :phone, :tags, :for_admin]
|
||||
if params.key?(:helpscout_form) # submitting form
|
||||
params.require(:helpscout_form).permit(*keys)
|
||||
else
|
||||
params.permit(:dossier_id, tags: []) # prefilling form
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -74,7 +74,7 @@ module ApplicationHelper
|
|||
tags, type, dossier_id = options.values_at(:tags, :type, :dossier_id)
|
||||
options.except!(:tags, :type, :dossier_id)
|
||||
|
||||
params = { tags: tags, type: type, dossier_id: dossier_id }.compact
|
||||
params = { tags: Array.wrap(tags), type: type, dossier_id: dossier_id }.compact
|
||||
link_to title, contact_url(params), options
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,9 @@ class HelpscoutCreateConversationJob < ApplicationJob
|
|||
|
||||
retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
|
||||
|
||||
def perform(blob_id: nil, **args)
|
||||
attr_reader :api
|
||||
|
||||
def perform(blob_id: nil, **params)
|
||||
if blob_id.present?
|
||||
blob = ActiveStorage::Blob.find(blob_id)
|
||||
raise FileNotScannedYetError if blob.virus_scanner.pending?
|
||||
|
@ -16,6 +18,31 @@ class HelpscoutCreateConversationJob < ApplicationJob
|
|||
blob = nil unless blob.virus_scanner.safe?
|
||||
end
|
||||
|
||||
Helpscout::FormAdapter.new(**args, blob:).send_form
|
||||
@api = Helpscout::API.new
|
||||
|
||||
create_conversation(params, blob)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_conversation(params, blob)
|
||||
response = api.create_conversation(
|
||||
params[:email],
|
||||
params[:subject],
|
||||
params[:text],
|
||||
blob
|
||||
)
|
||||
|
||||
if response.success?
|
||||
conversation_id = response.headers['Resource-ID']
|
||||
|
||||
if params[:phone].present?
|
||||
api.add_phone_number(params[:email], params[:phone])
|
||||
end
|
||||
|
||||
api.add_tags(conversation_id, params[:tags])
|
||||
else
|
||||
fail "Error while creating conversation: #{response.response_code} '#{response.body}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,39 @@
|
|||
class Helpscout::FormAdapter
|
||||
attr_reader :params
|
||||
class Helpscout::Form
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
|
||||
def self.options
|
||||
attribute :email, :string
|
||||
attribute :subject, :string
|
||||
attribute :text, :string
|
||||
attribute :type, :string
|
||||
attribute :dossier_id, :integer
|
||||
attribute :tags, :string
|
||||
attribute :phone, :string
|
||||
attribute :tags, :string
|
||||
attribute :for_admin, :boolean, default: false
|
||||
|
||||
validates :email, presence: true, strict_email: true, if: :require_email? # i18n-tasks-use t('activemodel.errors.models.helpscout/form.invalid_email_format')
|
||||
validates :subject, presence: true
|
||||
validates :text, presence: true
|
||||
validates :type, presence: true
|
||||
|
||||
attr_reader :current_user
|
||||
attr_reader :options
|
||||
|
||||
TYPE_INFO = 'procedure_info'
|
||||
TYPE_PERDU = 'lost_user'
|
||||
TYPE_INSTRUCTION = 'instruction_info'
|
||||
TYPE_AMELIORATION = 'product'
|
||||
TYPE_AUTRE = 'other'
|
||||
|
||||
ADMIN_TYPE_RDV = 'admin_demande_rdv'
|
||||
ADMIN_TYPE_QUESTION = 'admin_question'
|
||||
ADMIN_TYPE_SOUCIS = 'admin_soucis'
|
||||
ADMIN_TYPE_PRODUIT = 'admin_suggestion_produit'
|
||||
ADMIN_TYPE_DEMANDE_COMPTE = 'admin_demande_compte'
|
||||
ADMIN_TYPE_AUTRE = 'admin_autre'
|
||||
|
||||
def self.default_options
|
||||
[
|
||||
[I18n.t(:question, scope: [:support, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")],
|
||||
[I18n.t(:question, scope: [:support, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL],
|
||||
|
@ -22,60 +54,25 @@ class Helpscout::FormAdapter
|
|||
]
|
||||
end
|
||||
|
||||
def initialize(params = {}, api = nil)
|
||||
@params = params
|
||||
@api = api || Helpscout::API.new
|
||||
end
|
||||
def initialize(params)
|
||||
@current_user = params.delete(:current_user)
|
||||
params[:email] = EmailSanitizableConcern::EmailSanitizer.sanitize(params[:email]) if params[:email].present?
|
||||
super(params)
|
||||
|
||||
TYPE_INFO = 'procedure_info'
|
||||
TYPE_PERDU = 'lost_user'
|
||||
TYPE_INSTRUCTION = 'instruction_info'
|
||||
TYPE_AMELIORATION = 'product'
|
||||
TYPE_AUTRE = 'other'
|
||||
|
||||
ADMIN_TYPE_RDV = 'admin demande rdv'
|
||||
ADMIN_TYPE_QUESTION = 'admin question'
|
||||
ADMIN_TYPE_SOUCIS = 'admin soucis'
|
||||
ADMIN_TYPE_PRODUIT = 'admin suggestion produit'
|
||||
ADMIN_TYPE_DEMANDE_COMPTE = 'admin demande compte'
|
||||
ADMIN_TYPE_AUTRE = 'admin autre'
|
||||
|
||||
def send_form
|
||||
conversation_id = create_conversation
|
||||
|
||||
if conversation_id.present?
|
||||
add_tags(conversation_id)
|
||||
true
|
||||
@options = if for_admin?
|
||||
self.class.admin_options
|
||||
else
|
||||
false
|
||||
self.class.default_options
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
alias for_admin? for_admin
|
||||
|
||||
def add_tags(conversation_id)
|
||||
@api.add_tags(conversation_id, tags)
|
||||
def tags_array
|
||||
(tags&.split(",") || []) + ['contact form', type]
|
||||
end
|
||||
|
||||
def tags
|
||||
(params[:tags].presence || []) + ['contact form']
|
||||
end
|
||||
def require_email? = current_user.blank?
|
||||
|
||||
def create_conversation
|
||||
response = @api.create_conversation(
|
||||
params[:email],
|
||||
params[:subject],
|
||||
params[:text],
|
||||
params[:blob]
|
||||
)
|
||||
|
||||
if response.success?
|
||||
if params[:phone].present?
|
||||
@api.add_phone_number(params[:email], params[:phone])
|
||||
end
|
||||
response.headers['Resource-ID']
|
||||
else
|
||||
raise StandardError, "Error while creating conversation: #{response.response_code} '#{response.body}'"
|
||||
end
|
||||
end
|
||||
def persisted? = false
|
||||
end
|
58
app/views/support/_form.html.haml
Normal file
58
app/views/support/_form.html.haml
Normal file
|
@ -0,0 +1,58 @@
|
|||
= form_for form, url: contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do |f|
|
||||
%p.fr-hint-text= t('asterisk_html', scope: [:utils])
|
||||
|
||||
- if form.require_email?
|
||||
= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email' }) do |c|
|
||||
- c.with_label { Helpscout::Form.human_attribute_name(form.for_admin? ? :email_pro : :email) }
|
||||
|
||||
%fieldset.fr-fieldset{ name: "type" }
|
||||
%legend.fr-fieldset__legend.fr-fieldset__legend--regular
|
||||
= t('.your_question')
|
||||
= render EditableChamp::AsteriskMandatoryComponent.new
|
||||
.fr-fieldset__content
|
||||
- form.options.each do |(question, question_type, link)|
|
||||
.fr-radio-group
|
||||
= f.radio_button :type, question_type, required: true, data: {"support-target": "inputRadio" }, checked: question_type == form.type
|
||||
= f.label "type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do
|
||||
= question
|
||||
|
||||
- if link.present?
|
||||
.fr-ml-3w{ id: "card-#{question_type}",
|
||||
class: class_names('hidden' => question_type != form.type),
|
||||
"aria-hidden": question_type != form.type,
|
||||
data: { "support-target": "content" } }
|
||||
= render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c|
|
||||
- c.with_html_body do
|
||||
-# i18n-tasks-use t("support.index.#{question_type}.answer_html")
|
||||
= t('answer_html', scope: [:support, :index, question_type], base_url: Current.application_base_url, "link_#{question_type}": link)
|
||||
|
||||
|
||||
- if form.for_admin?
|
||||
= render Dsfr::InputComponent.new(form: f, attribute: :phone, required: false)
|
||||
- else
|
||||
= render Dsfr::InputComponent.new(form: f, attribute: :dossier_id, required: false)
|
||||
|
||||
= render Dsfr::InputComponent.new(form: f, attribute: :subject)
|
||||
|
||||
= render Dsfr::InputComponent.new(form: f, attribute: :text, input_type: :text_area, opts: { rows: 6 })
|
||||
|
||||
- if !form.for_admin?
|
||||
.fr-upload-group
|
||||
= f.label :piece_jointe, class: 'fr-label' do
|
||||
= t('pj', scope: [:utils])
|
||||
%span.fr-hint-text
|
||||
= t('.notice_upload_group')
|
||||
|
||||
%p.notice.hidden{ data: { 'contact-type-only': Helpscout::Form::TYPE_AMELIORATION } }
|
||||
= t('.notice_pj_product')
|
||||
%p.notice.hidden{ data: { 'contact-type-only': Helpscout::Form::TYPE_AUTRE } }
|
||||
= t('.notice_pj_other')
|
||||
= f.file_field :piece_jointe, class: 'fr-upload', accept: '.jpg, .jpeg, .png, .pdf'
|
||||
|
||||
= f.hidden_field :tags
|
||||
= f.hidden_field :for_admin
|
||||
|
||||
= invisible_captcha
|
||||
|
||||
.fr-input-group.fr-my-3w
|
||||
= f.submit t('send_mail', scope: [:utils]), type: :submit, class: 'fr-btn', data: { disable: true }
|
|
@ -1,49 +1,12 @@
|
|||
- content_for(:title, 'Contact')
|
||||
- content_for(:title, t('.contact_team'))
|
||||
- content_for :footer do
|
||||
= render partial: "root/footer"
|
||||
|
||||
#contact-form
|
||||
.fr-container
|
||||
%h1
|
||||
= t('.contact_team')
|
||||
|
||||
.description
|
||||
= t('.admin_intro_html', contact_path: contact_path)
|
||||
%br
|
||||
%p.mandatory-explanation= t('asterisk_html', scope: [:utils])
|
||||
.fr-highlight= t('.admin_intro_html', contact_path: contact_path)
|
||||
|
||||
= form_tag contact_path, method: :post, class: 'form' do |f|
|
||||
- if !user_signed_in?
|
||||
.contact-champ
|
||||
= label_tag :email do
|
||||
= t('.pro_mail')
|
||||
%span.mandatory *
|
||||
= text_field_tag :email, params[:email], required: true
|
||||
|
||||
.contact-champ
|
||||
= label_tag :type do
|
||||
= t('.your_question')
|
||||
%span.mandatory *
|
||||
= select_tag :type, options_for_select(@options, params[:type])
|
||||
|
||||
.contact-champ
|
||||
= label_tag :phone do
|
||||
= t('.pro_phone_number')
|
||||
= text_field_tag :phone
|
||||
|
||||
.contact-champ
|
||||
= label_tag :subject do
|
||||
= t('subject', scope: [:utils])
|
||||
= text_field_tag :subject, params[:subject], required: false
|
||||
|
||||
.contact-champ
|
||||
= label_tag :text do
|
||||
= t('message', scope: [:utils])
|
||||
%span.mandatory *
|
||||
= text_area_tag :text, params[:text], rows: 6, required: true
|
||||
|
||||
= invisible_captcha
|
||||
|
||||
= hidden_field_tag :tags, @tags&.join(',')
|
||||
= hidden_field_tag :admin, true
|
||||
|
||||
.send-wrapper
|
||||
= button_tag t('send_mail', scope: [:utils]), type: :submit, class: 'button send primary'
|
||||
= render partial: "form", object: @form
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
%h1
|
||||
= t('.contact')
|
||||
|
||||
= form_tag contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do
|
||||
.fr-highlight= t('.intro_html')
|
||||
|
||||
.description
|
||||
.recommandations
|
||||
|
|
|
@ -102,6 +102,7 @@ ignore_unused:
|
|||
- 'activerecord.models.*'
|
||||
- 'activerecord.attributes.*'
|
||||
- 'activemodel.attributes.map_filter.*'
|
||||
- 'activemodel.attributes.helpscout/form.*'
|
||||
- 'activerecord.errors.*'
|
||||
- 'errors.messages.blank'
|
||||
- 'errors.messages.content_type_invalid'
|
||||
|
|
|
@ -52,9 +52,6 @@ en:
|
|||
asterisk_html: "Fields marked by an asterisk ( <svg aria-label='required' class='icon mandatory' height='10' role='img' viewBox='0 0 1200 1200' width='10' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><desc>required</desc><path d='M489.838 29.354v443.603L68.032 335.894 0 545.285l421.829 137.086-260.743 358.876 178.219 129.398L600.048 811.84l260.673 358.806 178.146-129.398-260.766-358.783L1200 545.379l-68.032-209.403-421.899 137.07V29.443H489.84l-.002-.089z'></path></svg> ) are mandatory."
|
||||
mandatory_champs: All fields are mandatory.
|
||||
no_mandatory: (optional)
|
||||
file_number: File number
|
||||
subject: Subject
|
||||
message: Message
|
||||
send_mail: Send message
|
||||
new_tab: New tab
|
||||
helpers:
|
||||
|
|
|
@ -43,9 +43,6 @@ fr:
|
|||
asterisk_html: "Les champs suivis d’un astérisque ( <svg aria-label='obligatoire' class='icon mandatory' height='10' role='img' viewBox='0 0 1200 1200' width='10' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><desc>obligatoire</desc><path d='M489.838 29.354v443.603L68.032 335.894 0 545.285l421.829 137.086-260.743 358.876 178.219 129.398L600.048 811.84l260.673 358.806 178.146-129.398-260.766-358.783L1200 545.379l-68.032-209.403-421.899 137.07V29.443H489.84l-.002-.089z'></path></svg> ) sont obligatoires."
|
||||
mandatory_champs: Tous les champs sont obligatoires.
|
||||
no_mandatory: (facultatif)
|
||||
file_number: Numéro de dossier
|
||||
subject: Sujet
|
||||
message: Message
|
||||
send_mail: Envoyer le message
|
||||
new_tab: "Nouvel onglet"
|
||||
helpers:
|
||||
|
|
|
@ -1,4 +1,19 @@
|
|||
en:
|
||||
activemodel:
|
||||
attributes:
|
||||
helpscout/form:
|
||||
email: 'Your email address'
|
||||
email_pro: Professional email address
|
||||
phone: Professional phone number (direct line)
|
||||
subject: Subject
|
||||
text: Message
|
||||
dossier_id: File number
|
||||
hints:
|
||||
email: 'Example: address@mail.com'
|
||||
errors:
|
||||
models:
|
||||
helpscout/form:
|
||||
invalid_email_format: 'is not valid'
|
||||
support:
|
||||
index:
|
||||
contact: Contact
|
||||
|
@ -10,44 +25,51 @@ en:
|
|||
our_answer: Our answer
|
||||
notice_pj_product: A screenshot can help us identify the element to improve.
|
||||
notice_pj_other: A screenshot can help us identify the issue.
|
||||
notice_upload_group: "Maximum size: 200 MB. Supported formats: jpg, png, pdf."
|
||||
notice_upload_group: 'Maximum size: 200 MB. Supported formats: jpg, png, pdf.'
|
||||
index:
|
||||
contact: Contact
|
||||
intro_html:
|
||||
'<p>Contact us via this form and we will answer you as quickly as possible.</p>
|
||||
<p>Make sure you provide all the required information so we can help you in the best way.</p>'
|
||||
procedure_info:
|
||||
question: I've encountered a problem while completing my application
|
||||
answer_html: "<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled?
|
||||
<p>If you have questions about the information requested, <a href=\"%{link_procedure_info}\">contact the service in charge of the procedure (FAQ)</a>.</p>"
|
||||
answer_html:
|
||||
'<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled?
|
||||
<p>If you have questions about the information requested, <a href="%{link_procedure_info}">contact the service in charge of the procedure (FAQ)</a>.</p>'
|
||||
instruction_info:
|
||||
question: I have a question about the instruction of my application
|
||||
answer_html: "<p>If you have questions about the instruction of your application (response delay for example), <a href=\"%{link_instruction_info}\">contact directly the instructor via our mail system (FAQ)</a>.</p>
|
||||
<p>If you are facing technical issues on the website, use the form below. We will not be able to inform you about the instruction of your application.</p>"
|
||||
answer_html:
|
||||
'<p>If you have questions about the instruction of your application (response delay for example), <a href="%{link_instruction_info}">contact directly the instructor via our mail system (FAQ)</a>.</p>
|
||||
<p>If you are facing technical issues on the website, use the form below. We will not be able to inform you about the instruction of your application.</p>'
|
||||
product:
|
||||
question: I have an idea to improve the website
|
||||
answer_html: "<p>Got an idea? <a href=\"%{link_product}\" rel=\"noopener noreferrer\" target=\"_blank\" title=\"Please check our enhancement dashboard - New tab\">Please check our enhancement dashboard</a> :</p>
|
||||
answer_html:
|
||||
'<p>Got an idea? <a href="%{link_product}" rel="noopener noreferrer" target="_blank" title="Please check our enhancement dashboard - New tab">Please check our enhancement dashboard</a> :</p>
|
||||
<ul><li>Vote for your priority improvements</li>
|
||||
<li>Share your own ideas</li></ul>"
|
||||
<li>Share your own ideas</li></ul>'
|
||||
lost_user:
|
||||
question: I am having trouble finding the procedure I am looking for
|
||||
answer_html: "<p>We invite you to contact the administration in charge of the procedure so they can provide you the link.
|
||||
answer_html:
|
||||
'<p>We invite you to contact the administration in charge of the procedure so they can provide you the link.
|
||||
It should look like this: <code>%{base_url}/commencer/NOM_DE_LA_DEMARCHE</code>.</p>
|
||||
<p>You can <a href=\"%{link_lost_user}\">find here the most popular procedures (licence, detr, etc.)</a>.</p>"
|
||||
<p>You can <a href="%{link_lost_user}">find here the most popular procedures (licence, detr, etc.)</a>.</p>'
|
||||
other:
|
||||
question: Other topic
|
||||
admin:
|
||||
your_question: Your question
|
||||
admin_intro_html: "<p>As an administration, you can contact us through this form. We'll answer you as quickly as possibly by e-mail or phone.</p>
|
||||
admin_intro_html:
|
||||
'<p>As an administration, you can contact us through this form. We''ll answer you as quickly as possibly by e-mail or phone.</p>
|
||||
<p><strong>Caution, this form is dedicated to public bodies only.</strong>
|
||||
It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us <a href=\"%{contact_path}\">here</a>.</p>"
|
||||
It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us <a href="%{contact_path}">here</a>.</p>'
|
||||
contact_team: Contact our team
|
||||
pro_phone_number: Professional phone number (direct line)
|
||||
pro_mail: Professional email address
|
||||
admin question:
|
||||
admin_question:
|
||||
question: I have a question about %{app_name}
|
||||
admin demande rdv:
|
||||
admin_demande_rdv:
|
||||
question: I request an appointment for an online presentation of %{app_name}
|
||||
admin soucis:
|
||||
admin_soucis:
|
||||
question: I am facing a technical issue on %{app_name}
|
||||
admin suggestion produit:
|
||||
admin_suggestion_produit:
|
||||
question: I have a suggestion for an evolution
|
||||
admin demande compte:
|
||||
admin_demande_compte:
|
||||
question: I want to open an admin account with an Orange, Wanadoo, etc. email
|
||||
admin autre:
|
||||
admin_autre:
|
||||
question: Other topic
|
||||
|
|
|
@ -1,4 +1,19 @@
|
|||
fr:
|
||||
activemodel:
|
||||
attributes:
|
||||
helpscout/form:
|
||||
email: 'Votre adresse email'
|
||||
email_pro: Votre adresse email professionnelle
|
||||
phone: Numéro de téléphone professionnel (ligne directe)
|
||||
subject: Sujet
|
||||
text: Message
|
||||
dossier_id: Numéro de dossier
|
||||
hints:
|
||||
email: 'Exemple: adresse@mail.com'
|
||||
errors:
|
||||
models:
|
||||
helpscout/form:
|
||||
invalid_email_format: 'est invalide'
|
||||
support:
|
||||
index:
|
||||
contact: Contact
|
||||
|
@ -10,44 +25,52 @@ fr:
|
|||
our_answer: Notre réponse
|
||||
notice_pj_product: Une capture d’écran peut nous aider à identifier plus facilement l’endroit à améliorer.
|
||||
notice_pj_other: Une capture d’écran peut nous aider à identifier plus facilement le problème.
|
||||
notice_upload_group: "Taille maximale : 200 Mo. Formats supportés : jpg, png, pdf."
|
||||
notice_upload_group: 'Taille maximale : 200 Mo. Formats supportés : jpg, png, pdf.'
|
||||
index:
|
||||
contact: Contact
|
||||
intro_html:
|
||||
'<p>Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.</p>
|
||||
<p>Pensez bien à nous donner le plus d’informations possible pour que nous puissions vous aider au mieux.</p>'
|
||||
procedure_info:
|
||||
question: J’ai un problème lors du remplissage de mon dossier
|
||||
answer_html: "<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ?
|
||||
<p>Si vous avez des questions sur les informations à saisir, <a href=\"%{link_procedure_info}\">contactez les services en charge de la démarche (FAQ)</a>.</p>"
|
||||
answer_html:
|
||||
'<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ?
|
||||
<p>Si vous avez des questions sur les informations à saisir, <a href="%{link_procedure_info}">contactez les services en charge de la démarche (FAQ)</a>.</p>'
|
||||
instruction_info:
|
||||
question: J’ai une question sur l’instruction de mon dossier
|
||||
answer_html: "<p>Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à <a href=\"%{link_instruction_info}\">contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ)</a>.</p>
|
||||
<p>Si vous souhaitez poser une question pour un problème technique sur le site, utilisez le formulaire ci-dessous. Nous ne pourrons pas vous renseigner sur l’instruction de votre dossier.</p>"
|
||||
answer_html:
|
||||
'<p>Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à <a href="%{link_instruction_info}">contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ)</a>.</p>
|
||||
<p>Si vous souhaitez poser une question pour un problème technique sur le site, utilisez le formulaire ci-dessous. Nous ne pourrons pas vous renseigner sur l’instruction de votre dossier.</p>'
|
||||
product:
|
||||
question: J’ai une idée d’amélioration pour votre site
|
||||
answer_html: "<p>Une idée ? Pensez à <a href=\"%{link_product}\" rel=\"noopener noreferrer\" target=\"_blank\" title=\"Consulter notre tableau de bord des améliorations - Nouvel onglet\">consulter notre tableau de bord des améliorations</a> :</p>
|
||||
answer_html:
|
||||
'<p>Une idée ? Pensez à <a href="%{link_product}" rel="noopener noreferrer" target="_blank" title="Consulter notre tableau de bord des améliorations - Nouvel onglet">consulter notre tableau de bord des améliorations</a> :</p>
|
||||
<ul><li>Votez pour vos améliorations prioritaires,</li>
|
||||
<li>Proposez votre propre idée.</li></ul>"
|
||||
<li>Proposez votre propre idée.</li></ul>'
|
||||
lost_user:
|
||||
question: Je ne trouve pas la démarche que je veux faire
|
||||
answer_html: "<p>Nous vous invitons à contacter l’administration en charge de votre démarche pour qu’elle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : <code>%{base_url}/commencer/NOM_DE_LA_DEMARCHE</code>.</p>
|
||||
<p>Vous pouvez aussi <a href=\"%{link_lost_user}\">consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.)</a>.</p>"
|
||||
answer_html:
|
||||
'<p>Nous vous invitons à contacter l’administration en charge de votre démarche pour qu’elle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : <code>%{base_url}/commencer/NOM_DE_LA_DEMARCHE</code>.</p>
|
||||
<p>Vous pouvez aussi <a href="%{link_lost_user}">consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.)</a>.</p>'
|
||||
other:
|
||||
question: Autre sujet
|
||||
|
||||
admin:
|
||||
your_question: Votre question
|
||||
admin_intro_html: "<p>En tant qu’administration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.</p>
|
||||
admin_intro_html:
|
||||
'<p>En tant qu’administration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.</p>
|
||||
<p><strong>Attention, ce formulaire est réservé uniquement aux organismes publics.</strong>
|
||||
Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues d’utilité publique). Si c'est votre cas, rendez-vous sur notre
|
||||
<a href=\"%{contact_path}\">formulaire de contact public</a>.</p>"
|
||||
Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues d’utilité publique). Si c''est votre cas, rendez-vous sur notre
|
||||
<a href="%{contact_path}">formulaire de contact public</a>.</p>'
|
||||
contact_team: Contactez notre équipe
|
||||
pro_phone_number: Numéro de téléphone professionnel (ligne directe)
|
||||
pro_mail: Adresse e-mail professionnelle
|
||||
admin question:
|
||||
admin_question:
|
||||
question: J’ai une question sur %{app_name}
|
||||
admin demande rdv:
|
||||
admin_demande_rdv:
|
||||
question: Demande de RDV pour une présentation à distance de %{app_name}
|
||||
admin soucis:
|
||||
admin_soucis:
|
||||
question: J’ai un problème technique avec %{app_name}
|
||||
admin suggestion produit:
|
||||
admin_suggestion_produit:
|
||||
question: J’ai une proposition d’évolution
|
||||
admin demande compte:
|
||||
admin_demande_compte:
|
||||
question: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc.
|
||||
admin autre:
|
||||
admin_autre:
|
||||
question: Autre sujet
|
||||
|
|
|
@ -12,7 +12,7 @@ describe SupportController, type: :controller do
|
|||
get :index
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).not_to have_content("Email *")
|
||||
expect(response.body).not_to have_content("Votre adresse email")
|
||||
end
|
||||
|
||||
describe "with dossier" do
|
||||
|
@ -51,29 +51,29 @@ describe SupportController, type: :controller do
|
|||
|
||||
describe "send form" do
|
||||
subject do
|
||||
post :create, params: params
|
||||
post :create, params: { helpscout_form: params }
|
||||
end
|
||||
|
||||
context "when invisible captcha is ignored" do
|
||||
let(:params) { { subject: 'bonjour', text: 'un message' } }
|
||||
let(:params) { { subject: 'bonjour', text: 'un message', type: 'procedure_info' } }
|
||||
|
||||
it 'creates a conversation on HelpScout' do
|
||||
expect { subject }.to \
|
||||
change(Commentaire, :count).by(0).and \
|
||||
have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params))
|
||||
have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:type)))
|
||||
|
||||
expect(flash[:notice]).to match('Votre message a été envoyé.')
|
||||
expect(response).to redirect_to root_path(formulaire_contact_general_submitted: true)
|
||||
expect(response).to redirect_to root_path
|
||||
end
|
||||
|
||||
context 'when a drafted dossier is mentionned' do
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:user) { dossier.user }
|
||||
|
||||
subject do
|
||||
post :create, params: {
|
||||
let(:params) do
|
||||
{
|
||||
dossier_id: dossier.id,
|
||||
type: Helpscout::FormAdapter::TYPE_INSTRUCTION,
|
||||
type: Helpscout::Form::TYPE_INSTRUCTION,
|
||||
subject: 'bonjour',
|
||||
text: 'un message'
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ describe SupportController, type: :controller do
|
|||
have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(subject: 'bonjour', dossier_id: dossier.id))
|
||||
|
||||
expect(flash[:notice]).to match('Votre message a été envoyé.')
|
||||
expect(response).to redirect_to root_path(formulaire_contact_general_submitted: true)
|
||||
expect(response).to redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -93,10 +93,10 @@ describe SupportController, type: :controller do
|
|||
let(:dossier) { create(:dossier, :en_construction) }
|
||||
let(:user) { dossier.user }
|
||||
|
||||
subject do
|
||||
post :create, params: {
|
||||
let(:params) do
|
||||
{
|
||||
dossier_id: dossier.id,
|
||||
type: Helpscout::FormAdapter::TYPE_INSTRUCTION,
|
||||
type: Helpscout::Form::TYPE_INSTRUCTION,
|
||||
subject: 'bonjour',
|
||||
text: 'un message'
|
||||
}
|
||||
|
@ -118,7 +118,13 @@ describe SupportController, type: :controller do
|
|||
end
|
||||
|
||||
context "when invisible captcha is filled" do
|
||||
let(:params) { { subject: 'bonjour', text: 'un message', InvisibleCaptcha.honeypots.sample => 'boom' } }
|
||||
subject do
|
||||
post :create, params: {
|
||||
helpscout_form: { subject: 'bonjour', text: 'un message', type: 'procedure_info' },
|
||||
InvisibleCaptcha.honeypots.sample => 'boom'
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not create a conversation on HelpScout' do
|
||||
expect { subject }.not_to change(Commentaire, :count)
|
||||
expect(flash[:alert]).to eq(I18n.t('invisible_captcha.sentence_for_humans'))
|
||||
|
@ -133,7 +139,7 @@ describe SupportController, type: :controller do
|
|||
get :index
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to have_text("Email")
|
||||
expect(response.body).to have_text("Votre adresse email")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -147,18 +153,46 @@ describe SupportController, type: :controller do
|
|||
expect(response.body).to include(tag)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'send form' do
|
||||
subject do
|
||||
post :create, params: { helpscout_form: params }
|
||||
end
|
||||
|
||||
let(:params) { { subject: 'bonjour', email: "me@rspec.net", text: 'un message', type: 'procedure_info' } }
|
||||
|
||||
it 'creates a conversation on HelpScout' do
|
||||
expect { subject }.to \
|
||||
change(Commentaire, :count).by(0).and \
|
||||
have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:type)))
|
||||
|
||||
expect(flash[:notice]).to match('Votre message a été envoyé.')
|
||||
expect(response).to redirect_to root_path
|
||||
end
|
||||
|
||||
context "when email is invalid" do
|
||||
let(:params) { super().merge(email: "me@rspec") }
|
||||
|
||||
it 'creates a conversation on HelpScout' do
|
||||
expect { subject }.not_to have_enqueued_job(HelpscoutCreateConversationJob)
|
||||
expect(response.body).to include("Le champ « Votre adresse email » est invalide")
|
||||
expect(response.body).to include("bonjour")
|
||||
expect(response.body).to include("un message")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'contact admin' do
|
||||
subject do
|
||||
post :create, params: params
|
||||
post :create, params: { helpscout_form: params }
|
||||
end
|
||||
|
||||
let(:params) { { admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message' } }
|
||||
let(:params) { { for_admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message', type: 'admin question', phone: '06' } }
|
||||
|
||||
describe "when form is filled" do
|
||||
it "creates a conversation on HelpScout" do
|
||||
expect { subject }.to have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:admin)))
|
||||
expect { subject }.to have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:for_admin, :type)))
|
||||
expect(flash[:notice]).to match('Votre message a été envoyé.')
|
||||
end
|
||||
|
||||
|
@ -176,7 +210,9 @@ describe SupportController, type: :controller do
|
|||
end
|
||||
|
||||
describe "when invisible captcha is filled" do
|
||||
let(:params) { super().merge(InvisibleCaptcha.honeypots.sample => 'boom') }
|
||||
subject do
|
||||
post :create, params: { helpscout_form: params, InvisibleCaptcha.honeypots.sample => 'boom' }
|
||||
end
|
||||
|
||||
it 'does not create a conversation on HelpScout' do
|
||||
subject
|
||||
|
|
|
@ -1,16 +1,43 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe HelpscoutCreateConversationJob, type: :job do
|
||||
let(:args) { { email: 'sender@email.com' } }
|
||||
let(:api) { instance_double("Helpscout::API") }
|
||||
let(:email) { 'help@rspec.net' }
|
||||
let(:subject_text) { 'Bonjour' }
|
||||
let(:text) { "J'ai un pb" }
|
||||
let(:tags) { ["first tag"] }
|
||||
let(:phone) { nil }
|
||||
let(:params) {
|
||||
{
|
||||
email:,
|
||||
subject: subject_text,
|
||||
text:,
|
||||
tags:,
|
||||
phone:
|
||||
}
|
||||
}
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
allow(Helpscout::API).to receive(:new).and_return(api)
|
||||
allow(api).to receive(:create_conversation)
|
||||
.and_return(double(
|
||||
success?: true,
|
||||
headers: { 'Resource-ID' => 'new-conversation-id' }
|
||||
))
|
||||
allow(api).to receive(:add_tags)
|
||||
allow(api).to receive(:add_phone_number) if params[:phone].present?
|
||||
end
|
||||
|
||||
subject {
|
||||
described_class.perform_now(**params)
|
||||
}
|
||||
|
||||
context 'when blob_id is not present' do
|
||||
it 'sends the form without a file' do
|
||||
form_adapter = double('Helpscout::FormAdapter')
|
||||
allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob: nil))).and_return(form_adapter)
|
||||
expect(form_adapter).to receive(:send_form)
|
||||
|
||||
described_class.perform_now(**args)
|
||||
subject
|
||||
expect(api).to have_received(:create_conversation).with(email, subject_text, text, nil)
|
||||
expect(api).to have_received(:add_tags).with("new-conversation-id", tags)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -18,9 +45,11 @@ RSpec.describe HelpscoutCreateConversationJob, type: :job do
|
|||
let(:blob) {
|
||||
ActiveStorage::Blob.create_and_upload!(io: StringIO.new("toto"), filename: "toto.png")
|
||||
}
|
||||
let(:params) { super().merge(blob_id: blob.id) }
|
||||
|
||||
before do
|
||||
allow(blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: pending, safe?: safe))
|
||||
allow(ActiveStorage::Blob).to receive(:find).with(blob.id).and_return(blob)
|
||||
end
|
||||
|
||||
context 'when the file has not been scanned yet' do
|
||||
|
@ -28,9 +57,7 @@ RSpec.describe HelpscoutCreateConversationJob, type: :job do
|
|||
let(:safe) { false }
|
||||
|
||||
it 'reenqueue job' do
|
||||
expect {
|
||||
described_class.perform_now(blob_id: blob.id, **args)
|
||||
}.to have_enqueued_job(described_class).with(blob_id: blob.id, **args)
|
||||
expect { subject }.to have_enqueued_job(described_class).with(params)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -39,11 +66,8 @@ RSpec.describe HelpscoutCreateConversationJob, type: :job do
|
|||
let(:safe) { true }
|
||||
|
||||
it 'downloads the file and sends the form' do
|
||||
form_adapter = double('Helpscout::FormAdapter')
|
||||
allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob:))).and_return(form_adapter)
|
||||
allow(form_adapter).to receive(:send_form)
|
||||
|
||||
described_class.perform_now(blob_id: blob.id, **args)
|
||||
subject
|
||||
expect(api).to have_received(:create_conversation).with(email, subject_text, text, blob)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -51,13 +75,18 @@ RSpec.describe HelpscoutCreateConversationJob, type: :job do
|
|||
let(:pending) { false }
|
||||
let(:safe) { false }
|
||||
|
||||
it 'downloads the file and sends the form' do
|
||||
form_adapter = double('Helpscout::FormAdapter')
|
||||
allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob: nil))).and_return(form_adapter)
|
||||
allow(form_adapter).to receive(:send_form)
|
||||
it 'ignore the file' do
|
||||
subject
|
||||
expect(api).to have_received(:create_conversation).with(email, subject_text, text, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
described_class.perform_now(blob_id: blob.id, **args)
|
||||
end
|
||||
context 'with a phone' do
|
||||
let(:phone) { "06" }
|
||||
it 'associates the phone number' do
|
||||
subject
|
||||
expect(api).to have_received(:add_phone_number).with(email, phone)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
describe Helpscout::FormAdapter do
|
||||
describe '#send_form' do
|
||||
let(:api) { spy(double(:api)) }
|
||||
|
||||
context 'create_conversation' do
|
||||
before do
|
||||
allow(api).to receive(:create_conversation)
|
||||
.and_return(double(success?: true, headers: {}))
|
||||
described_class.new(params, api).send_form
|
||||
end
|
||||
|
||||
let(:params) {
|
||||
{
|
||||
email: email,
|
||||
subject: subject,
|
||||
text: text
|
||||
}
|
||||
}
|
||||
let(:email) { 'paul.chavard@beta.gouv.fr' }
|
||||
let(:subject) { 'Bonjour' }
|
||||
let(:text) { "J'ai un problem" }
|
||||
|
||||
it 'should call method' do
|
||||
expect(api).to have_received(:create_conversation)
|
||||
.with(email, subject, text, nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'add_tags' do
|
||||
before do
|
||||
allow(api).to receive(:create_conversation)
|
||||
.and_return(
|
||||
double(
|
||||
success?: true,
|
||||
headers: {
|
||||
'Resource-ID' => conversation_id
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
described_class.new(params, api).send_form
|
||||
end
|
||||
|
||||
let(:params) {
|
||||
{
|
||||
email: email,
|
||||
subject: subject,
|
||||
text: text,
|
||||
tags: tags
|
||||
}
|
||||
}
|
||||
let(:email) { 'paul.chavard@beta.gouv.fr' }
|
||||
let(:subject) { 'Bonjour' }
|
||||
let(:text) { "J'ai un problem" }
|
||||
let(:tags) { ['info demarche'] }
|
||||
let(:conversation_id) { '123' }
|
||||
|
||||
it 'should call method' do
|
||||
expect(api).to have_received(:create_conversation)
|
||||
.with(email, subject, text, nil)
|
||||
expect(api).to have_received(:add_tags)
|
||||
.with(conversation_id, tags + ['contact form'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'add_phone' do
|
||||
before do
|
||||
allow(api).to receive(:create_conversation)
|
||||
.and_return(
|
||||
double(
|
||||
success?: true,
|
||||
headers: {
|
||||
'Resource-ID' => conversation_id
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
described_class.new(params, api).send_form
|
||||
end
|
||||
|
||||
let(:params) {
|
||||
{
|
||||
email: email,
|
||||
subject: subject,
|
||||
text: text,
|
||||
phone: '0666666666'
|
||||
}
|
||||
}
|
||||
let(:phone) { '0666666666' }
|
||||
let(:email) { 'paul.chavard@beta.gouv.fr' }
|
||||
let(:subject) { 'Bonjour' }
|
||||
let(:text) { "J'ai un problem" }
|
||||
let(:tags) { ['info demarche'] }
|
||||
let(:conversation_id) { '123' }
|
||||
|
||||
it 'should call method' do
|
||||
expect(api).to have_received(:create_conversation)
|
||||
.with(email, subject, text, nil)
|
||||
expect(api).to have_received(:add_phone_number)
|
||||
.with(email, phone)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue