diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 31fc5746..c3a05191 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -17,11 +17,7 @@ class StartFormController < ApplicationController def update return redirect_to start_form_path(@template.slug) if @template.archived_at? - @submitter = Submitter.where(submission: @template.submissions.where(expire_at: Time.current..) - .or(@template.submissions.where(expire_at: nil)).where(archived_at: nil)) - .order(id: :desc) - .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } - .find_or_initialize_by(**submitter_params.compact_blank) + @submitter = find_or_initialize_submitter(@template, submitter_params) if @submitter.completed_at? redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) @@ -56,6 +52,15 @@ class StartFormController < ApplicationController private + def find_or_initialize_submitter(template, submitter_params) + Submitter.where(submission: template.submissions.where(expire_at: Time.current..) + .or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) + .order(id: :desc) + .where(declined_at: nil) + .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } + .find_or_initialize_by(**submitter_params.compact_blank) + end + def assign_submission_attributes(submitter, template) resubmit_submitter = if params[:resubmit].present? diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 87cc217f..87040e50 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -15,6 +15,7 @@ class SubmitFormController < ApplicationController return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at? return render :expired if @submitter.submission.expired? + return render :declined if @submitter.declined_at? Submitters.preload_with_pages(@submitter) @@ -56,6 +57,8 @@ class SubmitFormController < ApplicationController return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity end + return render json: { error: 'Form has been declined.' }, status: :unprocessable_entity if submitter.declined_at? + Submitters::SubmitValues.call(submitter, params, request) head :ok diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb new file mode 100644 index 00000000..0fb9657e --- /dev/null +++ b/app/controllers/submit_form_decline_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class SubmitFormDeclineController < ApplicationController + skip_before_action :authenticate_user! + skip_authorization_check + + def create + submitter = Submitter.find_by!(slug: params[:submit_form_slug]) + + return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? || + submitter.completed_at? || + submitter.submission.archived_at? || + submitter.submission.expired? || + submitter.submission.template.archived_at? + + ApplicationRecord.transaction do + submitter.update!(declined_at: Time.current) + + SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] }) + end + + user = submitter.submission.created_by_user || submitter.template.author + + SubmitterMailer.declined_email(submitter, user).deliver_later! + SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id) + + redirect_to submit_form_path(submitter.slug) + end +end diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb new file mode 100644 index 00000000..9be5c0ac --- /dev/null +++ b/app/controllers/submit_form_download_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class SubmitFormDownloadController < ApplicationController + skip_before_action :authenticate_user! + skip_authorization_check + + FILES_TTL = 5.minutes + + def index + submitter = Submitter.find_by!(slug: params[:submit_form_slug]) + + return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at? + + return head :unprocessable_entity if submitter.declined_at? || + submitter.submission.archived_at? || + submitter.submission.expired? || + submitter.submission.template.archived_at? + + last_completed_submitter = submitter.submission.submitters + .where.not(id: submitter.id) + .where.not(completed_at: nil) + .max_by(&:completed_at) + + attachments = + if last_completed_submitter + Submitters.select_attachments_for_download(last_completed_submitter) + else + submitter.submission.template.schema_documents.preload(:blob) + end + + urls = attachments.map do |attachment| + ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) + end + + render json: urls + end +end diff --git a/app/controllers/webhook_preferences_controller.rb b/app/controllers/webhook_preferences_controller.rb index 15a05be8..d584fdf9 100644 --- a/app/controllers/webhook_preferences_controller.rb +++ b/app/controllers/webhook_preferences_controller.rb @@ -5,6 +5,7 @@ class WebhookPreferencesController < ApplicationController form.viewed form.started form.completed + form.declined template.created template.updated submission.created diff --git a/app/jobs/send_form_declined_webhook_request_job.rb b/app/jobs/send_form_declined_webhook_request_job.rb new file mode 100644 index 00000000..b4f158b6 --- /dev/null +++ b/app/jobs/send_form_declined_webhook_request_job.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class SendFormDeclinedWebhookRequestJob + include Sidekiq::Job + + sidekiq_options queue: :webhooks + + USER_AGENT = 'DocuSeal.co Webhook' + + MAX_ATTEMPTS = 10 + + def perform(params = {}) + submitter = Submitter.find(params['submitter_id']) + + attempt = params['attempt'].to_i + config = Accounts.load_webhook_config(submitter.submission.account) + url = config&.value.presence + + return if url.blank? + + preferences = Accounts.load_webhook_preferences(submitter.submission.account) + + return if preferences['form.declined'] == false + + ActiveStorage::Current.url_options = Docuseal.default_url_options + + resp = begin + Faraday.post(url, + { + event_type: 'form.declined', + timestamp: Time.current, + data: Submitters::SerializeForWebhook.call(submitter) + }.to_json, + **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, + key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + 'Content-Type' => 'application/json', + 'User-Agent' => USER_AGENT) + rescue Faraday::Error + nil + end + + if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && + (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) + SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, { + 'submitter_id' => submitter.id, + 'attempt' => attempt + 1, + 'last_status' => resp&.status.to_i + }) + end + end +end diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index bfd5bef2..646d5324 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -78,6 +78,23 @@ class SubmitterMailer < ApplicationMailer subject:) end + def declined_email(submitter, user) + @current_account = submitter.submission.account + @submitter = submitter + @submission = submitter.submission + @user = user + + assign_message_metadata('submitter_declined', @submitter) + + I18n.with_locale(submitter.account.locale) do + mail(from: from_address_for_submitter(submitter), + to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name, + subject: I18n.t(:name_declined_by_submitter, + name: @submission.template.name.truncate(20), + submitter: @submitter.name || @submitter.email || @submitter.phone)) + end + end + def documents_copy_email(submitter, to: nil, sig: false) @current_account = submitter.submission.account @submitter = submitter diff --git a/app/models/submission_event.rb b/app/models/submission_event.rb index 17392d3d..8cfa11c1 100644 --- a/app/models/submission_event.rb +++ b/app/models/submission_event.rb @@ -48,6 +48,7 @@ class SubmissionEvent < ApplicationRecord start_form: 'start_form', view_form: 'view_form', complete_form: 'complete_form', + decline_form: 'decline_form', api_complete_form: 'api_complete_form' }, scope: false diff --git a/app/models/submitter.rb b/app/models/submitter.rb index 925cc26d..e41b66ce 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # completed_at :datetime +# declined_at :datetime # email :string # ip :string # metadata :text not null @@ -59,7 +60,9 @@ class Submitter < ApplicationRecord scope :completed, -> { where.not(completed_at: nil) } def status - if completed_at? + if declined_at? + 'declined' + elsif completed_at? 'completed' elsif opened_at? 'opened' @@ -83,6 +86,6 @@ class Submitter < ApplicationRecord end def status_event_at - completed_at || opened_at || sent_at || created_at + declined_at || completed_at || opened_at || sent_at || created_at end end diff --git a/app/views/shared/_html_modal.html.erb b/app/views/shared/_html_modal.html.erb new file mode 100644 index 00000000..a966b195 --- /dev/null +++ b/app/views/shared/_html_modal.html.erb @@ -0,0 +1,19 @@ +<% uuid = SecureRandom.uuid %> + + + diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 41d3b29e..f936221c 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -54,8 +54,8 @@ <% end %> - <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? %> - <%= render 'shared/clipboard_copy', text: start_form_url(slug: @submission.template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %> + <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %> + <%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %> <% end %> @@ -143,18 +143,30 @@
<%= svg_icon('writing', class: 'w-5 h-5') %> - <%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : 'Not completed yet' %> + <% if submitter&.declined_at? %> + Declined on <%= l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale) %> + <% else %> + <%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : 'Not completed yet' %> + <% end %>
- <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && Accounts.can_send_emails?(current_account) && !@submission.expired? %> + <% if submitter&.declined_at? %> +
+ + Reason: + <%= simple_format(submitter.submission_events.find_by(event_type: :decline_form).data['reason']) %> + +
+ <% end %> + <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
<% end %> - <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && !@submission.expired? %> + <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && !@submission.expired? && !submitter.declined_at? %> <%= render 'submissions/send_sms_button', submitter: %> <% end %> - <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, submitter) && !@submission.expired? %> + <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, submitter) && !@submission.expired? && !submitter.declined_at? %>
Sign In-person diff --git a/app/views/submit_form/_decline_form.html.erb b/app/views/submit_form/_decline_form.html.erb new file mode 100644 index 00000000..9795d39e --- /dev/null +++ b/app/views/submit_form/_decline_form.html.erb @@ -0,0 +1,11 @@ +<%= form_for '', url: submit_form_decline_index_path(submitter.slug), method: :post do |f| %> +
+ <%= t(:notify_the_sender_with_the_reason_you_declined) %> +
+
+ <%= f.text_area :reason, required: true, class: 'base-input w-full py-2 h-40', dir: 'auto', placeholder: t('provide_a_reason'), style: 'height: 200px', rows: '10' %> +
+ + <%= f.button button_title(title: t(:decline)), class: 'base-button' %> + +<% end %> diff --git a/app/views/submit_form/declined.html.erb b/app/views/submit_form/declined.html.erb new file mode 100644 index 00000000..f0e06b3f --- /dev/null +++ b/app/views/submit_form/declined.html.erb @@ -0,0 +1,21 @@ +
+
+
+
+ <%= render 'start_form/banner' %> +
+
+
+
+ <%= svg_icon('writing_sign', class: 'w-10 h-10') %> +
+
+

<%= @submitter.submission.template.name %>

+

<%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %>

+
+
+
+
+
+
+<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index cb70e844..38c40361 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -8,7 +8,30 @@
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %> - <%= local_assigns[:banner_html] || render('submit_form/banner') %> + <%= local_assigns[:banner_html] || capture do %> + <%= render('submit_form/banner') %> +
+
+ <%= @submitter.submission.template.name %> +
+
+
+ <%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5' do %> + <%= render 'submit_form/decline_form', submitter: @submitter %> + <% end %> +
+ + + <%= t('download') %> + + + +
+
+ <% end %> <% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %> <% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> diff --git a/app/views/submitter_mailer/declined_email.html.erb b/app/views/submitter_mailer/declined_email.html.erb new file mode 100644 index 00000000..38239092 --- /dev/null +++ b/app/views/submitter_mailer/declined_email.html.erb @@ -0,0 +1,4 @@ +

<%= t('hi_there') %>,

+

<%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %>

+<%= simple_format(@submitter.submission_events.find_by(event_type: :decline_form).data['reason']) %> +

<%= link_to t('view'), submission_url(@submitter.submission) %>

diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb index 8815fc11..de08e5f2 100644 --- a/app/views/templates/_submission.html.erb +++ b/app/views/templates/_submission.html.erb @@ -1,4 +1,4 @@ -<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %> +<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning', 'declined' => 'badge-error' } %>
<% if local_assigns[:with_template] %> <% template = submission.template %> @@ -69,7 +69,7 @@ - <% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? %> + <% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if current_user.email == submitter.email %>
- <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? %> + <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index 942bfd1e..9deda2d1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -18,6 +18,8 @@ en: &en language_nl: Nederlands language_ar: العربية language_ko: 한국어 + view: View + hi_there: Hi there email: Email form_expired_at_html: 'Form expired on %{time}' verification_code_code: 'Verification code: %{code}' @@ -39,6 +41,15 @@ en: &en or: or download_documents: Download documents downloading: Downloading + download: Download + decline: Decline + declined: Declined + decline_reason: Decline reason + provide_a_reason: Provide a reason + notify_the_sender_with_the_reason_you_declined: Notify the sender with the reason you declined + form_has_been_declined_on_html: 'Form has been declined on %{time}' + name_declined_by_submitter: '"%{name}" declined by %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" has been declined by %{submitter} with the following reason:' completed_successfully: Completed Successfully password: Password sign_in: Sign In @@ -65,6 +76,17 @@ en: &en read: Read your data es: &es + view: Vista + hi_there: Hola + download: Descargar + decline: Rechazar + declined: Rechazado + decline_reason: Motivo de rechazo + provide_a_reason: Proporcione un motivo + notify_the_sender_with_the_reason_you_declined: Notifique al remitente con el motivo por el que rechazó + form_has_been_declined_on_html: 'El formulario ha sido rechazado el %{time}' + name_declined_by_submitter: '"%{name}" rechazado por %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" ha sido rechazado por %{submitter} con el siguiente motivo:' role: Rol reason: Razón form_expired_at_html: 'El formulario expiró el %{time}' @@ -109,6 +131,17 @@ es: &es enter_email_to_continue: Ingresa tu correo electrónico para continuar it: &it + view: View + hi_there: Bonjour, + download: Télécharger + decline: Refuser + declined: Refusé + decline_reason: Raison du refus + provide_a_reason: Fournir une raison + notify_the_sender_with_the_reason_you_declined: Informer l'expéditeur de la raison pour laquelle vous avez refusé + form_has_been_declined_on_html: 'Le formulaire a été refusé le %{time}' + name_declined_by_submitter: '"%{name}" refusé par %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" a été refusé par %{submitter} pour la raison suivante :' role: Rôle reason: Ragione verification_code_code: 'Codice di verifica: %{code}' @@ -152,6 +185,17 @@ it: &it enter_email_to_continue: Inserisci l'email per continuare fr: &fr + view: Vue + hi_there: Salut, + download: Télécharger + decline: Refuser + declined: Refusé + decline_reason: Raison du refus + provide_a_reason: Fournir une raison + notify_the_sender_with_the_reason_you_declined: Informer l'expéditeur de la raison pour laquelle vous avez refusé + form_has_been_declined_on_html: 'Le formulaire a été refusé le %{time}' + name_declined_by_submitter: '"%{name}" a été refusé par %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" a été refusé par %{submitter} avec la raison suivante :' email: Email verification_code_code: 'Code de vérification: %{code}' digitally_signed_by: Signé numériquement par @@ -195,13 +239,23 @@ fr: &fr enter_email_to_continue: Entrez votre e-mail pour continuer pt: &pt + view: Visualizar + hi_there: Olá, + download: Baixar + decline: Recusar + declined: Recusado + decline_reason: Motivo da recusa + provide_a_reason: Forneça um motivo + notify_the_sender_with_the_reason_you_declined: Notifique o remetente com o motivo que você recusou + form_has_been_declined_on_html: 'O formulário foi recusado em %{time}' + name_declined_by_submitter: '"%{name}" foi recusado por %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" foi recusado por %{submitter} com o seguinte motivo:' role: Função email: Email reason: Razão verification_code_code: 'Código de verificação: %{code}' digitally_signed_by: Assinado digitalmente por form_expired_at_html: 'O formulário expirou em %{time}' - role: Função provide_your_email_to_start: Forneça o seu email para começar start: Iniciar starting: Iniciando @@ -238,13 +292,23 @@ pt: &pt enter_email_to_continue: Insira o e-mail para continuar de: &de + view: Ansicht + hi_there: Hallo, + download: Herunterladen + decline: Ablehnen + declined: Abgelehnt + decline_reason: Ablehnungsgrund + provide_a_reason: Geben Sie einen Grund an + notify_the_sender_with_the_reason_you_declined: Benachrichtigen Sie den Absender über den Grund für Ihre Ablehnung + form_has_been_declined_on_html: 'Das Formular wurde am %{time} abgelehnt' + name_declined_by_submitter: '"%{name}" wurde von %{submitter} abgelehnt' + name_declined_by_submitter_with_the_following_reason: '"%{name}" wurde von %{submitter} mit folgendem Grund abgelehnt:' role: Rolle verification_code_code: 'Verifizierungscode: %{code}' reason: Grund email: E-Mail form_expired_at_html: 'Das Formular ist am %{time} abgelaufen' digitally_signed_by: Digital signiert von - role: Rolle provide_your_email_to_start: Gib deine E-Mail-Adresse ein, um zu starten start: Starten starting: Starten @@ -281,7 +345,18 @@ de: &de enter_email_to_continue: E-Mail eingeben, um fortzufahren pl: - email: Email + view: Widok + hi_there: Cześć, + download: Pobierz + decline: Odrzuć + declined: Odrzucono + decline_reason: Powód odrzucenia + provide_a_reason: Podaj powód + notify_the_sender_with_the_reason_you_declined: Powiadom nadawcę o powodzie, dla którego odrzuciłeś + form_has_been_declined_on_html: 'Formularz został odrzucony o %{time}' + name_declined_by_submitter: '"%{name}" odrzucono przez %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" został odrzucony przez %{submitter} z następującym powodem:' + email: E-mail verification_code_code: 'Kod weryfikacyjny: %{code}' digitally_signed_by: Podpis cyfrowy przez form_expired_at_html: 'Formularz wygasł o %{time}' @@ -323,7 +398,18 @@ pl: enter_email_to_continue: Wprowadź e-mail, aby kontynuować uk: - email: Email + view: Переглянути + hi_there: Привіт, + download: Завантажити + decline: Відхилити + declined: Відхилено + decline_reason: Причина відхилення + provide_a_reason: Надайте причину + notify_the_sender_with_the_reason_you_declined: Повідомте відправника про причину, з якої ви відхилили + form_has_been_declined_on_html: 'Форму було відхилено о %{time}' + name_declined_by_submitter: '"%{name}" відхилено %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" було відхилено %{submitter} з такою причиною:' + email: E-mail digitally_signed_by: Цифровий підпис від verification_code_code: 'Код підтвердження: %{code}' role: Роль @@ -365,7 +451,18 @@ uk: enter_email_to_continue: Введіть електронну пошту, щоб продовжити cs: - email: Email + view: Zobrazit + hi_there: Ahoj, + download: Stáhnout + decline: Odmítnout + declined: Odmítnuto + decline_reason: Důvod odmítnutí + provide_a_reason: Uveďte důvod + notify_the_sender_with_the_reason_you_declined: Informujte odesílatele o důvodu odmítnutí + form_has_been_declined_on_html: 'Formulář byl odmítnut dne %{time}' + name_declined_by_submitter: '"%{name}" odmítnuto %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" bylo odmítnuto %{submitter} s následujícím důvodem:' + email: E-mail digitally_signed_by: Digitálně podepsáno uživatelem verification_code_code: 'Ověřovací kód: %{code}' role: Role @@ -407,6 +504,17 @@ cs: enter_email_to_continue: Zadejte e-mail pro pokračování he: + view: תצוגה + hi_there: שלום, + download: הורד + decline: דחייה + declined: דחוי + decline_reason: סיבת דחייה + provide_a_reason: אנא ספק סיבה + notify_the_sender_with_the_reason_you_declined: הודע לשולח על הסיבה לדחייה + form_has_been_declined_on_html: 'הטופס נדחה בתאריך %{time}' + name_declined_by_submitter: '"%{name}" נדחה על ידי %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" נדחה על ידי %{submitter} עם הסיבה הבאה:' email: דוא"ל digitally_signed_by: חתום דיגיטלית על ידי role: תפקיד @@ -449,6 +557,17 @@ he: enter_email_to_continue: הכנס דוא"ל כדי להמשיך nl: + view: Bekijken + hi_there: Hallo, + download: Downloaden + decline: Weigeren + declined: Geweigerd + decline_reason: Weigeringsreden + provide_a_reason: Geef een reden op + notify_the_sender_with_the_reason_you_declined: Informeer de afzender over de reden waarom je hebt geweigerd + form_has_been_declined_on_html: 'Formulier is afgewezen op %{time}' + name_declined_by_submitter: '"%{name}" is geweigerd door %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" is geweigerd door %{submitter} met de volgende reden:' email: E-mail digitally_signed_by: Digitaal ondertekend door role: Rol @@ -492,6 +611,17 @@ nl: enter_email_to_continue: Voer e-mail in om door te gaan ar: + view: عرض + hi_there: مرحبا, + download: تحميل + decline: رفض + declined: مرفوض + decline_reason: سبب الرفض + provide_a_reason: قدم سببًا + notify_the_sender_with_the_reason_you_declined: أخطر المرسل بسبب الرفض + form_has_been_declined_on_html: 'تم رفض النموذج في %{time}' + name_declined_by_submitter: '"%{name}" مرفوض بواسطة %{submitter}' + name_declined_by_submitter_with_the_following_reason: '"%{name}" تم رفضه بواسطة %{submitter} مع السبب التالي:' email: البريد الإلكتروني digitally_signed_by: تم التوقيع الرقمي بواسطة role: الدور @@ -535,6 +665,17 @@ ar: enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة ko: + view: 보기 + hi_there: 안녕하세요, + download: 다운로드 + decline: 거절 + declined: 거절됨 + decline_reason: 거절 사유 + provide_a_reason: 사유를 제공하세요 + notify_the_sender_with_the_reason_you_declined: 거절한 이유로 발신자에게 알리기 + form_has_been_declined_on_html: '양식이 %{time}에 거절되었습니다.' + name_declined_by_submitter: '"%{name}"이(가) %{submitter}에 의해 거절되었습니다.' + name_declined_by_submitter_with_the_following_reason: '"%{name}"이(가) %{submitter}에 의해 다음과 같은 사유로 거절되었습니다:' email: 이메일 digitally_signed_by: 디지털로 서명됨 role: 역할 diff --git a/config/routes.rb b/config/routes.rb index 0bea5270..d24756af 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -125,6 +125,8 @@ Rails.application.routes.draw do resources :submit_form, only: %i[show update], path: 's', param: 'slug' do resources :values, only: %i[index], controller: 'submit_form_values' + resources :download, only: %i[index], controller: 'submit_form_download' + resources :decline, only: %i[create], controller: 'submit_form_decline' get :completed end diff --git a/db/migrate/20240820180922_add_declined_at_to_submitters.rb b/db/migrate/20240820180922_add_declined_at_to_submitters.rb new file mode 100644 index 00000000..a8f1e9d2 --- /dev/null +++ b/db/migrate/20240820180922_add_declined_at_to_submitters.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDeclinedAtToSubmitters < ActiveRecord::Migration[7.1] + def change + add_column :submitters, :declined_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index de8df185..8a3123bc 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.1].define(version: 2024_08_16_121641) do +ActiveRecord::Schema[7.1].define(version: 2024_08_20_180922) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -244,6 +244,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_16_121641) do t.text "preferences", null: false t.text "metadata", null: false t.bigint "account_id", null: false + t.datetime "declined_at" t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id" t.index ["email"], name: "index_submitters_on_email" t.index ["external_id"], name: "index_submitters_on_external_id" diff --git a/lib/submissions/serialize_for_api.rb b/lib/submissions/serialize_for_api.rb index bfe36f7b..dcfac7fe 100644 --- a/lib/submissions/serialize_for_api.rb +++ b/lib/submissions/serialize_for_api.rb @@ -7,7 +7,7 @@ module Submissions methods: %i[audit_log_url], include: { submitters: { only: %i[id slug uuid name email phone - completed_at opened_at sent_at + completed_at opened_at sent_at declined_at created_at updated_at external_id metadata], methods: %i[status application_key] }, template: { only: %i[id name external_id created_at updated_at], @@ -41,7 +41,11 @@ module Submissions json[:completed_at] = last_submitter.completed_at else json[:documents] = [] - json[:status] = submission.expired? ? 'expired' : 'pending' + json[:status] = if submitters.any?(&:declined_at?) + 'declined' + else + submission.expired? ? 'expired' : 'pending' + end json[:completed_at] = nil end diff --git a/lib/submitters/serialize_for_api.rb b/lib/submitters/serialize_for_api.rb index d7030bce..9e5c7787 100644 --- a/lib/submitters/serialize_for_api.rb +++ b/lib/submitters/serialize_for_api.rb @@ -3,7 +3,7 @@ module Submitters module SerializeForApi SERIALIZE_PARAMS = { - only: %i[id slug uuid name email phone completed_at external_id + only: %i[id slug uuid name email phone completed_at declined_at external_id submission_id metadata opened_at sent_at created_at updated_at], methods: %i[status application_key] }.freeze diff --git a/lib/submitters/serialize_for_webhook.rb b/lib/submitters/serialize_for_webhook.rb index 47de6832..619ccb18 100644 --- a/lib/submitters/serialize_for_webhook.rb +++ b/lib/submitters/serialize_for_webhook.rb @@ -5,7 +5,7 @@ module Submitters SERIALIZE_PARAMS = { methods: %i[status application_key], only: %i[id submission_id email phone name ua ip sent_at opened_at - completed_at created_at updated_at external_id metadata] + completed_at declined_at created_at updated_at external_id metadata] }.freeze module_function @@ -34,7 +34,7 @@ module Submitters methods: %i[folder_name]), 'submission' => { **submitter.submission.slice(:id, :audit_log_url, :created_at), - status: submitter.submission.submitters.all?(&:completed_at?) ? 'completed' : 'pending', + status: build_submission_status(submitter.submission), url: r.submissions_preview_url(submitter.submission.slug, **Docuseal.default_url_options) }) end @@ -83,6 +83,18 @@ module Submitters end end + def build_submission_status(submission) + submitters = submission.submitters + + if submitters.all?(&:completed_at?) + 'completed' + elsif submitters.any?(&:declined_at?) + 'declined' + else + submission.expired? ? 'expired' : 'pending' + end + end + def build_documents_array(submitter) submitter.documents.map do |attachment| { name: attachment.filename.base, url: rails_storage_proxy_url(attachment) } diff --git a/spec/mailers/previews/submitter_mailer_preview.rb b/spec/mailers/previews/submitter_mailer_preview.rb index 708ff8a2..3fdb6b1d 100644 --- a/spec/mailers/previews/submitter_mailer_preview.rb +++ b/spec/mailers/previews/submitter_mailer_preview.rb @@ -11,6 +11,12 @@ class SubmitterMailerPreview < ActionMailer::Preview SubmitterMailer.completed_email(submitter, User.last) end + def declined_email + submitter = Submitter.where.not(declined_at: nil).last + + SubmitterMailer.declined_email(submitter, User.last) + end + def documents_copy_email submitter = Submitter.where.not(completed_at: nil).joins(:documents_attachments).last diff --git a/tailwind.form.config.js b/tailwind.form.config.js index 871af4b5..74e558c8 100644 --- a/tailwind.form.config.js +++ b/tailwind.form.config.js @@ -6,6 +6,7 @@ module.exports = { './app/javascript/submission_form/**/*.vue', './app/views/submit_form/**/*.erb', './app/views/start_form/**/*.erb', + './app/views/shared/_html_modal.html.erb', './app/views/shared/_button_title.html.erb', './app/views/shared/_attribution.html.erb', './app/views/scripts/_autosize_field.html.erb',