diff --git a/app/components/dsfr/input_errorable.rb b/app/components/dsfr/input_errorable.rb index 9ae359fd86b..c3851b49714 100644 --- a/app/components/dsfr/input_errorable.rb +++ b/app/components/dsfr/input_errorable.rb @@ -127,6 +127,8 @@ def default_hint end end + def hint? = hint.present? + def password? false end @@ -142,15 +144,6 @@ def autoresize? def hintable? false end - - def hint? - return true if get_slot(:hint).present? - - maybe_hint = I18n.exists?("activerecord.attributes.#{object.class.name.underscore}.hints.#{@attribute}") - maybe_hint_html = I18n.exists?("activerecord.attributes.#{object.class.name.underscore}.hints.#{@attribute}_html") - - maybe_hint || maybe_hint_html - end end end end diff --git a/app/controllers/contact_controller.rb b/app/controllers/contact_controller.rb new file mode 100644 index 00000000000..c037f47efee --- /dev/null +++ b/app/controllers/contact_controller.rb @@ -0,0 +1,76 @@ +class ContactController < ApplicationController + invisible_captcha only: [:create], on_spam: :redirect_to_root + + def index + @form = ContactForm.new(tags: contact_form_params.fetch(:tags, []), dossier_id: dossier&.id) + @form.user = current_user + end + + def admin + @form = ContactForm.new(tags: contact_form_params.fetch(:tags, []), for_admin: true) + @form.user = current_user + end + + def create + if direct_message? + create_commentaire! + flash.notice = t('.direct_message_sent') + + redirect_to messagerie_dossier_path(dossier) + return + end + + @form = ContactForm.new(contact_form_params) + @form.user = current_user + + if @form.save + @form.create_conversation_later + flash.notice = t('.message_sent') + + redirect_to root_path + else + flash.alert = @form.errors.full_messages + render @form.for_admin ? :admin : :index + end + end + + private + + def create_commentaire! + attributes = { + piece_jointe: contact_form_params[:piece_jointe], + body: "[#{contact_form_params[:subject]}]

#{contact_form_params[:text]}" + } + CommentaireService.create!(current_user, dossier, attributes) + end + + def browser_name + if browser.known? + "#{browser.name} #{browser.version} (#{browser.platform.name})" + end + end + + def direct_message? + return false unless user_signed_in? + return false unless contact_form_params[:question_type] == ContactForm::TYPE_INSTRUCTION + + dossier&.messagerie_available? + end + + def dossier + @dossier ||= current_user&.dossiers&.find_by(id: contact_form_params[:dossier_id]) + end + + def redirect_to_root + redirect_to root_path, alert: t('invisible_captcha.sentence_for_humans') + end + + def contact_form_params + keys = [:email, :subject, :text, :question_type, :dossier_id, :piece_jointe, :phone, :for_admin, tags: []] + if params.key?(:contact_form) # submitting form + params.require(:contact_form).permit(*keys) + else + params.permit(:dossier_id, tags: []) # prefilling form + end + end +end diff --git a/app/controllers/support_controller.rb b/app/controllers/support_controller.rb deleted file mode 100644 index e2e536499d8..00000000000 --- a/app/controllers/support_controller.rb +++ /dev/null @@ -1,101 +0,0 @@ -class SupportController < ApplicationController - invisible_captcha only: [:create], on_spam: :redirect_to_root - - def index - setup_context - end - - def admin - setup_context_admin - end - - def create - if direct_message? && create_commentaire - flash.notice = "Votre message a été envoyé sur la messagerie de votre dossier." - - redirect_to messagerie_dossier_path(dossier) - return - end - - create_conversation_later - flash.notice = "Votre message a été envoyé." - - if params[:admin] - redirect_to root_path(formulaire_contact_admin_submitted: true) - else - redirect_to root_path(formulaire_contact_general_submitted: true) - 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] - blob = ActiveStorage::Blob.create_and_upload!( - io: params[:piece_jointe].tempfile, - filename: params[:piece_jointe].original_filename, - content_type: 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, - browser: browser_name, - tags: tags - ) - end - - def create_commentaire - attributes = { - piece_jointe: params[:piece_jointe], - body: "[#{params[:subject]}]

#{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 direct_message? - user_signed_in? && params[:type] == Helpscout::FormAdapter::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] - end - - def redirect_to_root - redirect_to root_path, alert: t('invisible_captcha.sentence_for_humans') - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 177d5196ed3..3cfe2b1c152 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -74,7 +74,7 @@ def contact_link(title, options = {}) 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 diff --git a/app/javascript/controllers/support_controller.ts b/app/javascript/controllers/contact_controller.ts similarity index 95% rename from app/javascript/controllers/support_controller.ts rename to app/javascript/controllers/contact_controller.ts index 22d947f0e37..436adc913a1 100644 --- a/app/javascript/controllers/support_controller.ts +++ b/app/javascript/controllers/contact_controller.ts @@ -1,7 +1,7 @@ import { ApplicationController } from './application_controller'; import { hide, show } from '@utils'; -export class SupportController extends ApplicationController { +export class ContactController extends ApplicationController { static targets = ['inputRadio', 'content']; declare readonly inputRadioTargets: HTMLInputElement[]; diff --git a/app/jobs/helpscout_create_conversation_job.rb b/app/jobs/helpscout_create_conversation_job.rb index a8175d0294c..f12cab839ab 100644 --- a/app/jobs/helpscout_create_conversation_job.rb +++ b/app/jobs/helpscout_create_conversation_job.rb @@ -8,14 +8,49 @@ class FileNotScannedYetError < StandardError retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 - def perform(blob_id: nil, **args) - if blob_id.present? - blob = ActiveStorage::Blob.find(blob_id) - raise FileNotScannedYetError if blob.virus_scanner.pending? + attr_reader :contact_form + attr_reader :api - blob = nil unless blob.virus_scanner.safe? + def perform(contact_form) + @contact_form = contact_form + + if contact_form.piece_jointe.attached? + raise FileNotScannedYetError if contact_form.piece_jointe.virus_scanner.pending? end - Helpscout::FormAdapter.new(**args, blob:).send_form + @api = Helpscout::API.new + + create_conversation + + contact_form.destroy + end + + private + + def create_conversation + response = api.create_conversation( + contact_form.email, + contact_form.subject, + contact_form.text, + safe_blob + ) + + if response.success? + conversation_id = response.headers['Resource-ID'] + + if contact_form.phone.present? + api.add_phone_number(contact_form.email, contact_form.phone) + end + + api.add_tags(conversation_id, contact_form.tags) + else + fail "Error while creating conversation: #{response.response_code} '#{response.body}'" + end + end + + def safe_blob + return if !contact_form.piece_jointe.virus_scanner&.safe? + + contact_form.piece_jointe end end diff --git a/app/lib/helpscout/form_adapter.rb b/app/lib/helpscout/form_adapter.rb deleted file mode 100644 index 4127e9dc252..00000000000 --- a/app/lib/helpscout/form_adapter.rb +++ /dev/null @@ -1,81 +0,0 @@ -class Helpscout::FormAdapter - attr_reader :params - - def self.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], - [I18n.t(:question, scope: [:support, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")], - [I18n.t(:question, scope: [:support, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL], - [I18n.t(:question, scope: [:support, :index, TYPE_AUTRE]), TYPE_AUTRE] - ] - end - - def self.admin_options - [ - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE] - ] - end - - def initialize(params = {}, api = nil) - @params = params - @api = api || Helpscout::API.new - end - - 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 - else - false - end - end - - private - - def add_tags(conversation_id) - @api.add_tags(conversation_id, tags) - end - - def tags - (params[:tags].presence || []) + ['contact form'] - end - - 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 -end diff --git a/app/models/contact_form.rb b/app/models/contact_form.rb new file mode 100644 index 00000000000..4e518e59891 --- /dev/null +++ b/app/models/contact_form.rb @@ -0,0 +1,81 @@ +class ContactForm < ApplicationRecord + attr_reader :options + + belongs_to :user, optional: true, dependent: :destroy + + after_initialize :set_options + before_validation :normalize_strings + before_validation :sanitize_email + before_save :add_default_tags + + validates :email, presence: true, strict_email: true, if: :require_email? + validates :subject, presence: true + validates :text, presence: true + validates :question_type, presence: true + + has_one_attached :piece_jointe + + 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: [:contact, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")], + [I18n.t(:question, scope: [:contact, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL], + [I18n.t(:question, scope: [:contact, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")], + [I18n.t(:question, scope: [:contact, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL], + [I18n.t(:question, scope: [:contact, :index, TYPE_AUTRE]), TYPE_AUTRE] + ] + end + + def self.admin_options + [ + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE] + ] + end + + def for_admin=(value) + super(value) + set_options + end + + def create_conversation_later + HelpscoutCreateConversationJob.perform_later(self) + end + + def require_email? = user.blank? + + private + + def normalize_strings + self.subject = subject&.strip + self.text = text&.strip + end + + def sanitize_email + self.email = EmailSanitizableConcern::EmailSanitizer.sanitize(email) if email.present? + end + + def add_default_tags + self.tags = tags.push('contact form', question_type).uniq + end + + def set_options + @options = for_admin? ? self.class.admin_options : self.class.default_options + end +end diff --git a/app/views/contact/_form.html.haml b/app/views/contact/_form.html.haml new file mode 100644 index 00000000000..c284df56682 --- /dev/null +++ b/app/views/contact/_form.html.haml @@ -0,0 +1,60 @@ += form_for form, url: contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :contact } 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 { ContactForm.human_attribute_name(form.for_admin? ? :email_pro : :email) } + + %fieldset.fr-fieldset{ name: "question_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 :question_type, question_type, required: true, data: {"contact-target": "inputRadio" }, checked: question_type == form.question_type + = f.label "question_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.question_type), + "aria-hidden": question_type != form.question_type, + data: { "contact-target": "content" } } + = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c| + - c.with_html_body do + -# i18n-tasks-use t("contact.index.#{question_type}.answer_html") + = t('answer_html', scope: [:contact, :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': ContactForm::TYPE_AMELIORATION } } + = t('.notice_pj_product') + %p.notice.hidden{ data: { 'contact-type-only': ContactForm::TYPE_AUTRE } } + = t('.notice_pj_other') + = f.file_field :piece_jointe, class: 'fr-upload', accept: '.jpg, .jpeg, .png, .pdf' + + - f.object.tags.each_with_index do |tag, index| + = f.hidden_field :tags, name: f.field_name(:tags, multiple: true), id: f.field_id(:tag, index), value: tag + + = 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 } diff --git a/app/views/contact/admin.html.haml b/app/views/contact/admin.html.haml new file mode 100644 index 00000000000..1771256fc49 --- /dev/null +++ b/app/views/contact/admin.html.haml @@ -0,0 +1,12 @@ +- content_for(:title, t('.contact_team')) +- content_for :footer do + = render partial: "root/footer" + +#contact-form + .fr-container + %h1 + = t('.contact_team') + + .fr-highlight= t('.admin_intro_html', contact_path: contact_path) + + = render partial: "form", object: @form diff --git a/app/views/contact/index.html.haml b/app/views/contact/index.html.haml new file mode 100644 index 00000000000..0a33c87f180 --- /dev/null +++ b/app/views/contact/index.html.haml @@ -0,0 +1,12 @@ +- content_for(:title, t('.contact')) +- content_for :footer do + = render partial: "root/footer" + +#contact-form + .fr-container + %h1 + = t('.contact') + + .fr-highlight= t('.intro_html') + + = render partial: "form", object: @form diff --git a/app/views/support/admin.html.haml b/app/views/support/admin.html.haml deleted file mode 100644 index 28a53bc7243..00000000000 --- a/app/views/support/admin.html.haml +++ /dev/null @@ -1,49 +0,0 @@ -- content_for(:title, 'Contact') - -#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]) - - = 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' diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml deleted file mode 100644 index 26727bcbeb3..00000000000 --- a/app/views/support/index.html.haml +++ /dev/null @@ -1,77 +0,0 @@ -- content_for(:title, t('.contact')) -- content_for :footer do - = render partial: "root/footer" - -#contact-form - .fr-container - %h1 - = t('.contact') - - = form_tag contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do - - .description - .recommandations - = t('.intro_html') - %p.mandatory-explanation= t('asterisk_html', scope: [:utils]) - - - if !user_signed_in? - .fr-input-group - = label_tag :email, class: 'fr-label' do - Email - = render EditableChamp::AsteriskMandatoryComponent.new - %span.fr-hint-text - = t('.notice_email') - = email_field_tag :email, params[:email], required: true, autocomplete: 'email', class: 'fr-input' - - %fieldset.fr-fieldset{ name: "type" } - %legend.fr-fieldset__legend - = t('.your_question') - = render EditableChamp::AsteriskMandatoryComponent.new - .fr-fieldset__content - - @options.each do |(question, question_type, link)| - .fr-radio-group - = radio_button_tag :type, question_type, false, required: true, data: {"support-target": "inputRadio" } - = label_tag "type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do - = question - - - if link.present? - .fr-ml-3w.hidden{ id: "card-#{question_type}", "aria-hidden": true , 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) - - .fr-input-group - = label_tag :dossier_id, t('file_number', scope: [:utils]), class: 'fr-label' - = text_field_tag :dossier_id, @dossier_id, class: 'fr-input' - - .fr-input-group - = label_tag :subject, class: 'fr-label' do - = t('subject', scope: [:utils]) - = render EditableChamp::AsteriskMandatoryComponent.new - = text_field_tag :subject, params[:subject], required: true, class: 'fr-input' - - .fr-input-group - = label_tag :text, class: 'fr-label' do - = t('message', scope: [:utils]) - = render EditableChamp::AsteriskMandatoryComponent.new - = text_area_tag :text, params[:text], rows: 6, required: true, class: 'fr-input' - - .fr-upload-group - = label_tag :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::FormAdapter::TYPE_AMELIORATION } } - = t('.notice_pj_product') - %p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AUTRE } } - = t('.notice_pj_other') - = file_field_tag :piece_jointe, class: 'fr-upload', accept: '.jpg, .jpeg, .png, .pdf' - - = hidden_field_tag :tags, @tags&.join(',') - - = invisible_captcha - - .send-wrapper.fr-my-3w - = button_tag t('send_mail', scope: [:utils]), type: :submit, class: 'fr-btn send' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 82d29a1c910..f2a6e6cc65d 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -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' diff --git a/config/locales/en.yml b/config/locales/en.yml index 9737fcab837..be6edf0fc52 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -52,9 +52,6 @@ en: asterisk_html: "Fields marked by an asterisk ( required ) 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: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2c6469d62e4..dfcb5d7b816 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -43,9 +43,6 @@ fr: asterisk_html: "Les champs suivis d’un astérisque ( obligatoire ) 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: diff --git a/config/locales/views/contact/en.yml b/config/locales/views/contact/en.yml new file mode 100644 index 00000000000..c0815a28707 --- /dev/null +++ b/config/locales/views/contact/en.yml @@ -0,0 +1,73 @@ +en: + activerecord: + attributes: + contact_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: + contact_form: + invalid_email_format: 'is not valid' + contact: + form: + your_question: Your question + 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.' + index: + contact: Contact + intro_html: + '

Contact us via this form and we will answer you as quickly as possible.

+

Make sure you provide all the required information so we can help you in the best way.

' + procedure_info: + question: I've encountered a problem while completing my application + answer_html: + '

Are you sure that all the mandatory fields ( * ) are properly filled? +

If you have questions about the information requested, contact the service in charge of the procedure (FAQ).

' + instruction_info: + question: I have a question about the instruction of my application + answer_html: + '

If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system (FAQ).

+

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.

' + product: + question: I have an idea to improve the website + answer_html: + '

Got an idea? Please check our enhancement dashboard :

+ ' + lost_user: + question: I am having trouble finding the procedure I am looking for + answer_html: + '

We invite you to contact the administration in charge of the procedure so they can provide you the link. + It should look like this: %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

+

You can find here the most popular procedures (licence, detr, etc.).

' + other: + question: Other topic + admin: + admin_intro_html: + '

As an administration, you can contact us through this form. We''ll answer you as quickly as possibly by e-mail or phone.

+

Caution, this form is dedicated to public bodies only. + It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us here.

' + contact_team: Contact our team + admin_question: + question: I have a question about %{app_name} + admin_demande_rdv: + question: I request an appointment for an online presentation of %{app_name} + admin_soucis: + question: I am facing a technical issue on %{app_name} + admin_suggestion_produit: + question: I have a suggestion for an evolution + admin_demande_compte: + question: I want to open an admin account with an Orange, Wanadoo, etc. email + admin_autre: + question: Other topic + create: + direct_message_sent: Your message has been sent to the mailbox in your file. + message_sent: Your message has been sent. diff --git a/config/locales/views/contact/fr.yml b/config/locales/views/contact/fr.yml new file mode 100644 index 00000000000..6d5b3d62991 --- /dev/null +++ b/config/locales/views/contact/fr.yml @@ -0,0 +1,74 @@ +fr: + activerecord: + attributes: + contact_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: + contact_form: + invalid_email_format: 'est invalide' + contact: + form: + your_question: Votre question + 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.' + index: + contact: Contact + intro_html: + '

Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.

+

Pensez bien à nous donner le plus d’informations possible pour que nous puissions vous aider au mieux.

' + procedure_info: + question: J’ai un problème lors du remplissage de mon dossier + answer_html: + '

Avez-vous bien vérifié que tous les champs obligatoires ( * ) sont remplis ? +

Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche (FAQ).

' + instruction_info: + question: J’ai une question sur l’instruction de mon dossier + answer_html: + '

Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ).

+

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.

' + product: + question: J’ai une idée d’amélioration pour votre site + answer_html: + '

Une idée ? Pensez à consulter notre tableau de bord des améliorations :

+ ' + lost_user: + question: Je ne trouve pas la démarche que je veux faire + answer_html: + '

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 : %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

+

Vous pouvez aussi consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.).

' + other: + question: Autre sujet + + admin: + admin_intro_html: + '

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.

+

Attention, ce formulaire est réservé uniquement aux organismes publics. + 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 + formulaire de contact public.

' + contact_team: Contactez notre équipe + admin_question: + question: J’ai une question sur %{app_name} + admin_demande_rdv: + question: Demande de RDV pour une présentation à distance de %{app_name} + admin_soucis: + question: J’ai un problème technique avec %{app_name} + admin_suggestion_produit: + question: J’ai une proposition d’évolution + admin_demande_compte: + question: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc. + admin_autre: + question: Autre sujet + create: + direct_message_sent: Votre message a été envoyé sur la messagerie de votre dossier. + message_sent: Votre message a été envoyé. diff --git a/config/locales/views/support/en.yml b/config/locales/views/support/en.yml deleted file mode 100644 index 97012927680..00000000000 --- a/config/locales/views/support/en.yml +++ /dev/null @@ -1,53 +0,0 @@ -en: - support: - index: - contact: Contact - intro_html: - '

Contact us via this form and we will answer you as quickly as possible.

-

Make sure you provide all the required information so we can help you in the best way.

' - notice_email: 'Expected format: address@mail.com' - your_question: Your question - 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." - procedure_info: - question: I've encountered a problem while completing my application - answer_html: "

Are you sure that all the mandatory fields ( * ) are properly filled? -

If you have questions about the information requested, contact the service in charge of the procedure (FAQ).

" - instruction_info: - question: I have a question about the instruction of my application - answer_html: "

If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system (FAQ).

-

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.

" - product: - question: I have an idea to improve the website - answer_html: "

Got an idea? Please check our enhancement dashboard :

- " - lost_user: - question: I am having trouble finding the procedure I am looking for - answer_html: "

We invite you to contact the administration in charge of the procedure so they can provide you the link. - It should look like this: %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

-

You can find here the most popular procedures (licence, detr, etc.).

" - other: - question: Other topic - admin: - your_question: Your question - admin_intro_html: "

As an administration, you can contact us through this form. We'll answer you as quickly as possibly by e-mail or phone.

-

Caution, this form is dedicated to public bodies only. - It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us here.

" - contact_team: Contact our team - pro_phone_number: Professional phone number (direct line) - pro_mail: Professional email address - admin question: - question: I have a question about %{app_name} - admin demande rdv: - question: I request an appointment for an online presentation of %{app_name} - admin soucis: - question: I am facing a technical issue on %{app_name} - admin suggestion produit: - question: I have a suggestion for an evolution - admin demande compte: - question: I want to open an admin account with an Orange, Wanadoo, etc. email - admin autre: - question: Other topic diff --git a/config/locales/views/support/fr.yml b/config/locales/views/support/fr.yml deleted file mode 100644 index 02f520625f9..00000000000 --- a/config/locales/views/support/fr.yml +++ /dev/null @@ -1,53 +0,0 @@ -fr: - support: - index: - contact: Contact - intro_html: - '

Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.

-

Pensez bien à nous donner le plus d’informations possible pour que nous puissions vous aider au mieux.

' - notice_email: 'Format attendu : adresse@mail.com' - your_question: Votre question - 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." - procedure_info: - question: J’ai un problème lors du remplissage de mon dossier - answer_html: "

Avez-vous bien vérifié que tous les champs obligatoires ( * ) sont remplis ? -

Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche (FAQ).

" - instruction_info: - question: J’ai une question sur l’instruction de mon dossier - answer_html: "

Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ).

-

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.

" - product: - question: J’ai une idée d’amélioration pour votre site - answer_html: "

Une idée ? Pensez à consulter notre tableau de bord des améliorations :

- " - lost_user: - question: Je ne trouve pas la démarche que je veux faire - answer_html: "

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 : %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

-

Vous pouvez aussi consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.).

" - other: - question: Autre sujet - admin: - your_question: Votre question - admin_intro_html: "

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.

-

Attention, ce formulaire est réservé uniquement aux organismes publics. - 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 - formulaire de contact public.

" - 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: - question: J’ai une question sur %{app_name} - admin demande rdv: - question: Demande de RDV pour une présentation à distance de %{app_name} - admin soucis: - question: J’ai un problème technique avec %{app_name} - admin suggestion produit: - question: J’ai une proposition d’évolution - admin demande compte: - question: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc. - admin autre: - question: Autre sujet diff --git a/config/routes.rb b/config/routes.rb index 9c223926774..cc1f9f7b37f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -224,10 +224,10 @@ get "suivi" => "root#suivi" post "save_locale" => "root#save_locale" - get "contact", to: "support#index" - post "contact", to: "support#create" + get "contact", to: "contact#index" + post "contact", to: "contact#create" - get "contact-admin", to: "support#admin" + get "contact-admin", to: "contact#admin" get "mentions-legales", to: "static_pages#legal_notice" get "declaration-accessibilite", to: "static_pages#accessibility_statement" diff --git a/db/migrate/20240729160650_create_contact_forms.rb b/db/migrate/20240729160650_create_contact_forms.rb new file mode 100644 index 00000000000..7100760f7f4 --- /dev/null +++ b/db/migrate/20240729160650_create_contact_forms.rb @@ -0,0 +1,17 @@ +class CreateContactForms < ActiveRecord::Migration[7.0] + def change + create_table :contact_forms do |t| + t.string :email + t.string :subject, null: false + t.text :text, null: false + t.string :question_type, null: false + t.references :user, null: true, foreign_key: true + t.bigint :dossier_id # not a reference (dossier may not exist) + t.string :phone + t.string :tags, array: true, default: [] + t.boolean :for_admin, default: false, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0544b546ca6..49687353a56 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_07_16_091043) do +ActiveRecord::Schema[7.0].define(version: 2024_07_29_160650) do # These are extensions that must be enabled in order to support this database enable_extension "pg_buffercache" enable_extension "pg_stat_statements" @@ -315,6 +315,21 @@ t.index ["instructeur_id"], name: "index_commentaires_on_instructeur_id" end + create_table "contact_forms", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "dossier_id" + t.string "email" + t.boolean "for_admin", default: false, null: false + t.string "phone" + t.string "question_type", null: false + t.string "subject", null: false + t.string "tags", default: [], array: true + t.text "text", null: false + t.datetime "updated_at", null: false + t.bigint "user_id" + t.index ["user_id"], name: "index_contact_forms_on_user_id" + end + create_table "contact_informations", force: :cascade do |t| t.text "adresse", null: false t.datetime "created_at", null: false @@ -1229,6 +1244,7 @@ add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "experts" add_foreign_key "commentaires", "instructeurs" + add_foreign_key "contact_forms", "users" add_foreign_key "contact_informations", "groupe_instructeurs" add_foreign_key "dossier_assignments", "dossiers" add_foreign_key "dossier_batch_operations", "batch_operations" diff --git a/spec/controllers/contact_controller_spec.rb b/spec/controllers/contact_controller_spec.rb new file mode 100644 index 00000000000..44bafc57de6 --- /dev/null +++ b/spec/controllers/contact_controller_spec.rb @@ -0,0 +1,257 @@ +describe ContactController, question_type: :controller do + render_views + + context 'signed in' do + before do + sign_in user + end + + let(:user) { create(:user) } + + it 'should not have email field' do + get :index + + expect(response.status).to eq(200) + expect(response.body).not_to have_content("Votre adresse email") + end + + describe "with dossier" do + let(:user) { dossier.user } + let(:dossier) { create(:dossier) } + + it 'should fill dossier_id' do + get :index, params: { dossier_id: dossier.id } + + expect(response.status).to eq(200) + expect(response.body).to include((dossier.id).to_s) + end + end + + describe "with tag" do + let(:tag) { 'yolo' } + + it 'should fill tags' do + get :index, params: { tags: [tag] } + + expect(response.status).to eq(200) + expect(response.body).to include(tag) + end + end + + describe "with multiple tags" do + let(:tags) { ['yolo', 'toto'] } + + it 'should fill tags' do + get :index, params: { tags: tags } + + expect(response.status).to eq(200) + expect(response.body).to include("value=\"yolo\"") + expect(response.body).to include("value=\"toto\"") + end + end + + describe "send form" do + subject do + post :create, params: { contact_form: params } + end + + context "when invisible captcha is ignored" do + let(:params) { { subject: 'bonjour', text: 'un message', question_type: 'procedure_info' } } + + it 'creates a conversation on HelpScout' do + expect { subject }.to \ + change(Commentaire, :count).by(0).and \ + change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + + expect(contact_form.subject).to eq("bonjour") + expect(contact_form.text).to eq("un message") + expect(contact_form.tags).to include("procedure_info") + + expect(flash[:notice]).to match('Votre message a été envoyé.') + expect(response).to redirect_to root_path + end + + context 'when a drafted dossier is mentionned' do + let(:dossier) { create(:dossier) } + let(:user) { dossier.user } + + let(:params) do + { + dossier_id: dossier.id, + question_type: ContactForm::TYPE_INSTRUCTION, + subject: 'bonjour', + text: 'un message' + } + end + + it 'creates a conversation on HelpScout' do + expect { subject }.to \ + change(Commentaire, :count).by(0).and \ + change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + expect(contact_form.dossier_id).to eq(dossier.id) + + expect(flash[:notice]).to match('Votre message a été envoyé.') + expect(response).to redirect_to root_path + end + end + + context 'when a submitted dossier is mentionned' do + let(:dossier) { create(:dossier, :en_construction) } + let(:user) { dossier.user } + + let(:params) do + { + dossier_id: dossier.id, + question_type: ContactForm::TYPE_INSTRUCTION, + subject: 'bonjour', + text: 'un message' + } + end + + it 'posts the message to the dossier messagerie' do + expect { subject }.to change(Commentaire, :count).by(1) + assert_no_enqueued_jobs(only: HelpscoutCreateConversationJob) + + expect(Commentaire.last.email).to eq(user.email) + expect(Commentaire.last.dossier).to eq(dossier) + expect(Commentaire.last.body).to include('[bonjour]') + expect(Commentaire.last.body).to include('un message') + + expect(flash[:notice]).to match('Votre message a été envoyé sur la messagerie de votre dossier.') + expect(response).to redirect_to messagerie_dossier_path(dossier) + end + end + end + + context "when invisible captcha is filled" do + subject do + post :create, params: { + contact_form: { subject: 'bonjour', text: 'un message', question_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')) + end + end + end + end + + context 'signed out' do + describe "with dossier" do + it 'should have email field' do + get :index + + expect(response.status).to eq(200) + expect(response.body).to have_text("Votre adresse email") + end + end + + describe "with dossier" do + let(:tag) { 'yolo' } + + it 'should fill tags' do + get :index, params: { tags: [tag] } + + expect(response.status).to eq(200) + expect(response.body).to include(tag) + end + end + + describe 'send form' do + subject do + post :create, params: { contact_form: params } + end + + let(:params) { { subject: 'bonjour', email: "me@rspec.net", text: 'un message', question_type: 'procedure_info' } } + + it 'creates a conversation on HelpScout' do + expect { subject }.to \ + change(Commentaire, :count).by(0).and \ + change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + expect(contact_form.email).to eq("me@rspec.net") + + 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 + context 'index' do + it 'should have professionnal email field' do + get :admin + expect(response.body).to have_text('Votre adresse email professionnelle') + expect(response.body).to have_text('téléphone') + expect(response.body).to include('for_admin') + end + end + + context 'create' do + subject do + post :create, params: { contact_form: params } + end + + let(:params) { { for_admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message', question_type: 'admin question', phone: '06' } } + + describe "when form is filled" do + it "creates a conversation on HelpScout" do + expect { subject }.to change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + expect(contact_form.email).to eq(params[:email]) + expect(contact_form.phone).to eq("06") + expect(contact_form.tags).to match_array(["admin question", "contact form"]) + + expect(flash[:notice]).to match('Votre message a été envoyé.') + end + + context "with a piece justificative" do + let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') } + let(:params) { super().merge(piece_jointe: logo) } + + it "create blob and pass it to conversation job" do + expect { subject }.to change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(contact_form.piece_jointe).to be_attached + end + end + end + + describe "when invisible captcha is filled" do + subject do + post :create, params: { contact_form: params, InvisibleCaptcha.honeypots.sample => 'boom' } + end + + it 'does not create a conversation on HelpScout' do + subject + expect(flash[:alert]).to eq(I18n.t('invisible_captcha.sentence_for_humans')) + end + end + end + end +end diff --git a/spec/controllers/support_controller_spec.rb b/spec/controllers/support_controller_spec.rb deleted file mode 100644 index 281c7e4a3dc..00000000000 --- a/spec/controllers/support_controller_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -describe SupportController, type: :controller do - render_views - - context 'signed in' do - before do - sign_in user - end - - let(:user) { create(:user) } - - it 'should not have email field' do - get :index - - expect(response.status).to eq(200) - expect(response.body).not_to have_content("Email *") - end - - describe "with dossier" do - let(:user) { dossier.user } - let(:dossier) { create(:dossier) } - - it 'should fill dossier_id' do - get :index, params: { dossier_id: dossier.id } - - expect(response.status).to eq(200) - expect(response.body).to include((dossier.id).to_s) - end - end - - describe "with tag" do - let(:tag) { 'yolo' } - - it 'should fill tags' do - get :index, params: { tags: [tag] } - - expect(response.status).to eq(200) - expect(response.body).to include(tag) - end - end - - describe "with multiple tags" do - let(:tags) { ['yolo', 'toto'] } - - it 'should fill tags' do - get :index, params: { tags: tags } - - expect(response.status).to eq(200) - expect(response.body).to include(tags.join(',')) - end - end - - describe "send form" do - subject do - post :create, params: params - end - - context "when invisible captcha is ignored" do - let(:params) { { subject: 'bonjour', text: 'un message' } } - - it 'creates a conversation on HelpScout' do - expect { subject }.to \ - change(Commentaire, :count).by(0).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params)) - - expect(flash[:notice]).to match('Votre message a été envoyé.') - expect(response).to redirect_to root_path(formulaire_contact_general_submitted: true) - end - - context 'when a drafted dossier is mentionned' do - let(:dossier) { create(:dossier) } - let(:user) { dossier.user } - - subject do - post :create, params: { - dossier_id: dossier.id, - type: Helpscout::FormAdapter::TYPE_INSTRUCTION, - subject: 'bonjour', - text: 'un message' - } - end - - it 'creates a conversation on HelpScout' do - expect { subject }.to \ - change(Commentaire, :count).by(0).and \ - 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) - end - end - - context 'when a submitted dossier is mentionned' do - let(:dossier) { create(:dossier, :en_construction) } - let(:user) { dossier.user } - - subject do - post :create, params: { - dossier_id: dossier.id, - type: Helpscout::FormAdapter::TYPE_INSTRUCTION, - subject: 'bonjour', - text: 'un message' - } - end - - it 'posts the message to the dossier messagerie' do - expect { subject }.to change(Commentaire, :count).by(1) - assert_no_enqueued_jobs(only: HelpscoutCreateConversationJob) - - expect(Commentaire.last.email).to eq(user.email) - expect(Commentaire.last.dossier).to eq(dossier) - expect(Commentaire.last.body).to include('[bonjour]') - expect(Commentaire.last.body).to include('un message') - - expect(flash[:notice]).to match('Votre message a été envoyé sur la messagerie de votre dossier.') - expect(response).to redirect_to messagerie_dossier_path(dossier) - end - end - end - - context "when invisible captcha is filled" do - let(:params) { { subject: 'bonjour', text: 'un message', InvisibleCaptcha.honeypots.sample => 'boom' } } - 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')) - end - end - end - end - - context 'signed out' do - describe "with dossier" do - it 'should have email field' do - get :index - - expect(response.status).to eq(200) - expect(response.body).to have_text("Email") - end - end - - describe "with dossier" do - let(:tag) { 'yolo' } - - it 'should fill tags' do - get :index, params: { tags: [tag] } - - expect(response.status).to eq(200) - expect(response.body).to include(tag) - end - end - end - - context 'contact admin' do - subject do - post :create, params: params - end - - let(:params) { { admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message' } } - - 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(flash[:notice]).to match('Votre message a été envoyé.') - end - - context "with a piece justificative" do - let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') } - let(:params) { super().merge(piece_jointe: logo) } - - it "create blob and pass it to conversation job" do - expect { subject }.to \ - change(ActiveStorage::Blob, :count).by(1).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(blob_id: Integer)).and \ - have_enqueued_job(VirusScannerJob) - end - end - end - - describe "when invisible captcha is filled" do - let(:params) { super().merge(InvisibleCaptcha.honeypots.sample => 'boom') } - - it 'does not create a conversation on HelpScout' do - subject - expect(flash[:alert]).to eq(I18n.t('invisible_captcha.sentence_for_humans')) - end - end - end -end diff --git a/spec/factories/contact_form.rb b/spec/factories/contact_form.rb new file mode 100644 index 00000000000..a0da9e2bffc --- /dev/null +++ b/spec/factories/contact_form.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :contact_form do + user { nil } + email { 'test@example.com' } + dossier_id { nil } + subject { 'Test Subject' } + text { 'Test Content' } + question_type { 'lost_user' } + tags { ['test tag'] } + phone { nil } + end +end diff --git a/spec/jobs/helpscout_create_conversation_job_spec.rb b/spec/jobs/helpscout_create_conversation_job_spec.rb index 1220e538d64..5b45084f4d9 100644 --- a/spec/jobs/helpscout_create_conversation_job_spec.rb +++ b/spec/jobs/helpscout_create_conversation_job_spec.rb @@ -1,64 +1,86 @@ 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(:question_type) { "lost" } + let(:phone) { nil } + let(:contact_form) { create(:contact_form, email:, subject: subject_text, text:, tags:, phone:, question_type:) } describe '#perform' do - 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) + 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 phone.present? + end - described_class.perform_now(**args) + subject { + described_class.perform_now(contact_form) + } + + context 'when no file is attached' do + it 'sends the form without a file' do + 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", match_array(tags.concat(["contact form", question_type]))) + expect(contact_form).to be_destroyed end end - context 'when blob_id is present' do - let(:blob) { - ActiveStorage::Blob.create_and_upload!(io: StringIO.new("toto"), filename: "toto.png") - } - + context 'when a file is attached' do before do - allow(blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: pending, safe?: safe)) + file = fixture_file_upload('spec/fixtures/files/white.png', 'image/png') + contact_form.piece_jointe.attach(file) end context 'when the file has not been scanned yet' do - let(:pending) { true } - let(:safe) { false } + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: true, safe?: false)) + end - 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) + it 'reenqueues job' do + expect { subject }.to have_enqueued_job(described_class).with(contact_form) end end context 'when the file is safe' do - let(:pending) { false } - 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) + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: false, safe?: true)) + end - described_class.perform_now(blob_id: blob.id, **args) + it 'sends the form with the file' do + subject + expect(api).to have_received(:create_conversation).with(email, subject_text, text, contact_form.piece_jointe) end end context 'when the file is not safe' 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) + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: false, safe?: false)) + end - described_class.perform_now(blob_id: blob.id, **args) + it 'ignores the file' do + subject + expect(api).to have_received(:create_conversation).with(email, subject_text, text, nil) end end 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 end diff --git a/spec/lib/helpscout/form_adapter_spec.rb b/spec/lib/helpscout/form_adapter_spec.rb deleted file mode 100644 index 8eaa085a622..00000000000 --- a/spec/lib/helpscout/form_adapter_spec.rb +++ /dev/null @@ -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