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 %> + + +