Merge from docusealco/wip
This commit is contained in:
@@ -6,6 +6,7 @@ jobs:
|
||||
rubocop:
|
||||
name: Rubocop
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
@@ -30,6 +31,7 @@ jobs:
|
||||
erblint:
|
||||
name: Erblint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
@@ -54,6 +56,7 @@ jobs:
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js
|
||||
@@ -80,6 +83,7 @@ jobs:
|
||||
brakeman:
|
||||
name: Brakeman
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
@@ -107,6 +111,7 @@ jobs:
|
||||
rspec:
|
||||
name: RSpec
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -34,6 +34,7 @@ gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rotp'
|
||||
gem 'rouge', require: false
|
||||
gem 'rqrcode'
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
|
||||
@@ -453,6 +453,7 @@ GEM
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
@@ -632,6 +633,7 @@ DEPENDENCIES
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rotp
|
||||
rouge
|
||||
rqrcode
|
||||
rspec-rails
|
||||
rubocop
|
||||
|
||||
@@ -63,12 +63,7 @@ module Api
|
||||
|
||||
submissions = create_submissions(@template, params)
|
||||
|
||||
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
@@ -96,10 +91,7 @@ module Api
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
end
|
||||
|
||||
render json: @submission.as_json(only: %i[id archived_at])
|
||||
@@ -112,9 +104,9 @@ module Api
|
||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
if params[:template_folder].present?
|
||||
folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:template_folder])
|
||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id)
|
||||
|
||||
submissions = folder ? submissions.joins(:template).where(template: { folder_id: folder.id }) : submissions.none
|
||||
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids })
|
||||
end
|
||||
|
||||
if params.key?(:archived)
|
||||
|
||||
@@ -13,10 +13,7 @@ module Api
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||
|
||||
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||
|
||||
render json: {}
|
||||
end
|
||||
|
||||
@@ -28,10 +28,7 @@ module Api
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(cloned_template)
|
||||
|
||||
|
||||
@@ -67,10 +67,7 @@ module Api
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
render json: @template.as_json(only: %i[id updated_at])
|
||||
end
|
||||
@@ -95,9 +92,9 @@ module Api
|
||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
if params[:folder].present?
|
||||
folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:folder])
|
||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id)
|
||||
|
||||
templates = folder ? templates.where(folder:) : templates.none
|
||||
templates = templates.where(folder_id: folder_ids)
|
||||
end
|
||||
|
||||
templates
|
||||
|
||||
@@ -51,7 +51,7 @@ class StartFormController < ApplicationController
|
||||
|
||||
if @submitter.errors.blank? && @submitter.save
|
||||
if is_new_record
|
||||
enqueue_submission_create_webhooks(@submitter)
|
||||
WebhookUrls.enqueue_events(@submitter.submission, 'submission.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(@submitter)
|
||||
|
||||
@@ -107,13 +107,6 @@ class StartFormController < ApplicationController
|
||||
redirect_to start_form_path(@template.slug)
|
||||
end
|
||||
|
||||
def enqueue_submission_create_webhooks(submitter)
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class SubmissionsController < ApplicationController
|
||||
params: params.merge('send_completed_email' => true))
|
||||
end
|
||||
|
||||
enqueue_submission_created_webhooks(@template, submissions)
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
@@ -77,10 +77,7 @@ class SubmissionsController < ApplicationController
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
|
||||
I18n.t('submission_has_been_archived')
|
||||
end
|
||||
@@ -97,15 +94,6 @@ class SubmissionsController < ApplicationController
|
||||
template.save!
|
||||
end
|
||||
|
||||
def enqueue_submission_created_webhooks(template, submissions)
|
||||
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ class SubmissionsPreviewController < ApplicationController
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||
if use_signature?(@submission) && !signature_valid
|
||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||
@@ -48,9 +48,15 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def submission_valid_ttl?(submission)
|
||||
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||
def use_signature?(submission)
|
||||
return false if current_user && can?(:read, submission)
|
||||
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
|
||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||
|
||||
!submission_valid_ttl?(submission)
|
||||
end
|
||||
|
||||
def submission_valid_ttl?(submission)
|
||||
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||
|
||||
last_submitter && last_submitter.completed_at > TTL.ago
|
||||
|
||||
@@ -25,10 +25,7 @@ class SubmitFormDeclineController < ApplicationController
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
end
|
||||
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
|
||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
end
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
class TemplateFoldersController < ApplicationController
|
||||
load_and_authorize_resource :template_folder
|
||||
|
||||
helper_method :selected_order
|
||||
|
||||
def show
|
||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||
.preload(:author, :template_accesses)
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
end
|
||||
@@ -25,6 +27,16 @@ class TemplateFoldersController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def selected_order
|
||||
@selected_order ||=
|
||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||
'created_at'
|
||||
else
|
||||
cookies.permanent[:dashboard_templates_order]
|
||||
end
|
||||
end
|
||||
|
||||
def template_folder_params
|
||||
params.require(:template_folder).permit(:name)
|
||||
end
|
||||
|
||||
@@ -74,7 +74,7 @@ class TemplatesController < ApplicationController
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
enqueue_template_created_webhooks(@template)
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
else
|
||||
@@ -91,7 +91,7 @@ class TemplatesController < ApplicationController
|
||||
|
||||
SearchEntries.enqueue_reindex(@template) if is_name_changed
|
||||
|
||||
enqueue_template_updated_webhooks(@template)
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
head :ok
|
||||
end
|
||||
@@ -142,20 +142,6 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_template_created_webhooks(template)
|
||||
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_template_updated_webhooks(template)
|
||||
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def load_base_template
|
||||
return if params[:base_template_id].blank?
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ class TemplatesDashboardController < ApplicationController
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
|
||||
helper_method :selected_order
|
||||
|
||||
def index
|
||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
||||
|
||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||
@template_folders = sort_template_folders(@template_folders, current_user,
|
||||
cookies.permanent[:dashboard_templates_order])
|
||||
@template_folders = sort_template_folders(@template_folders, current_user, selected_order)
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
@@ -26,7 +27,7 @@ class TemplatesDashboardController < ApplicationController
|
||||
else
|
||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||
@templates = filter_templates(@templates).preload(:author, :template_accesses)
|
||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||
|
||||
limit =
|
||||
if @template_folders.size < 4
|
||||
@@ -56,7 +57,7 @@ class TemplatesDashboardController < ApplicationController
|
||||
rel = Template.where(
|
||||
Template.arel_table[:id].in(
|
||||
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
|
||||
.union(shared_template_ids.arel)
|
||||
.union(:all, shared_template_ids.arel)
|
||||
)
|
||||
)
|
||||
else
|
||||
@@ -101,6 +102,16 @@ class TemplatesDashboardController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def selected_order
|
||||
@selected_order ||=
|
||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||
'created_at'
|
||||
else
|
||||
cookies.permanent[:dashboard_templates_order]
|
||||
end
|
||||
end
|
||||
|
||||
def load_related_submissions
|
||||
@related_submissions = Submission.accessible_by(current_ability)
|
||||
.left_joins(:template)
|
||||
|
||||
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
@template.update!(schema:)
|
||||
|
||||
enqueue_template_created_webhooks(@template)
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
@@ -68,11 +68,4 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
{ files: [file] }
|
||||
end
|
||||
|
||||
def enqueue_template_created_webhooks(template)
|
||||
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookEventsController < ApplicationController
|
||||
load_and_authorize_resource :webhook_url, parent: false, id_param: :webhook_id
|
||||
before_action :load_webhook_event
|
||||
|
||||
def show
|
||||
return unless current_ability.can?(:read, @webhook_event.record)
|
||||
|
||||
@data =
|
||||
case @webhook_event.event_type
|
||||
when 'form.started', 'form.completed', 'form.declined', 'form.viewed'
|
||||
Submitters::SerializeForWebhook.call(@webhook_event.record)
|
||||
when 'submission.created', 'submission.completed', 'submission.expired'
|
||||
Submissions::SerializeForApi.call(@webhook_event.record)
|
||||
when 'template.created', 'template.updated'
|
||||
Templates::SerializeForApi.call(@webhook_event.record)
|
||||
when 'submission.archived'
|
||||
@webhook_event.record.as_json(only: %i[id archived_at])
|
||||
end
|
||||
end
|
||||
|
||||
def resend
|
||||
id_key = WebhookUrls::EVENT_TYPE_ID_KEYS.fetch(@webhook_event.event_type.split('.').first)
|
||||
|
||||
last_attempt_id = @webhook_event.webhook_attempts.maximum(:id)
|
||||
|
||||
WebhookUrls::EVENT_TYPE_TO_JOB_CLASS[@webhook_event.event_type].perform_async(
|
||||
id_key => @webhook_event.record_id,
|
||||
'webhook_url_id' => @webhook_event.webhook_url_id,
|
||||
'event_uuid' => @webhook_event.uuid,
|
||||
'attempt' => SendWebhookRequest::MANUAL_ATTEMPT,
|
||||
'last_status' => 0
|
||||
)
|
||||
|
||||
render turbo_stream: [
|
||||
turbo_stream.after(
|
||||
params[:button_id],
|
||||
helpers.tag.submit_form(
|
||||
helpers.button_to('', refresh_settings_webhook_event_path(@webhook_url.id, @webhook_event.uuid),
|
||||
params: { last_attempt_id: }),
|
||||
class: 'hidden', data: { interval: 3_000 }
|
||||
)
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
def refresh
|
||||
return head :ok if @webhook_event.webhook_attempts.maximum(:id) == params[:last_attempt_id].to_i
|
||||
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace(helpers.dom_id(@webhook_event),
|
||||
partial: 'event_row',
|
||||
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
|
||||
turbo_stream.replace("drawer_events_#{helpers.dom_id(@webhook_event)}",
|
||||
partial: 'drawer_events',
|
||||
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
|
||||
]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_webhook_event
|
||||
@webhook_event = @webhook_url.webhook_events.find_by!(uuid: params[:id])
|
||||
end
|
||||
end
|
||||
@@ -8,10 +8,26 @@ class WebhookSettingsController < ApplicationController
|
||||
@webhook_urls = @webhook_urls.order(id: :desc)
|
||||
@webhook_url = @webhook_urls.first_or_initialize
|
||||
|
||||
render @webhook_urls.size > 1 ? 'index' : 'show'
|
||||
if @webhook_urls.size > 1
|
||||
render :index
|
||||
else
|
||||
@webhook_events = @webhook_url.webhook_events
|
||||
|
||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||
|
||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@webhook_events = @webhook_url.webhook_events
|
||||
|
||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||
|
||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
@@ -37,13 +53,18 @@ class WebhookSettingsController < ApplicationController
|
||||
def resend
|
||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||
|
||||
authorize!(:read, submitter)
|
||||
|
||||
if submitter.blank? || @webhook_url.blank?
|
||||
return redirect_back(fallback_location: settings_webhooks_path,
|
||||
alert: I18n.t('unable_to_resend_webhook_request'))
|
||||
end
|
||||
|
||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => @webhook_url.id)
|
||||
SendTestWebhookRequestJob.perform_async(
|
||||
'submitter_id' => submitter.id,
|
||||
'event_uuid' => SecureRandom.uuid,
|
||||
'webhook_url_id' => @webhook_url.id
|
||||
)
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
||||
end
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelector('form').requestSubmit()
|
||||
if (this.dataset.interval) {
|
||||
this.interval = setInterval(() => {
|
||||
this.querySelector('form').requestSubmit()
|
||||
}, parseInt(this.dataset.interval))
|
||||
} else {
|
||||
this.querySelector('form').requestSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const en = {
|
||||
must_be_characters_length: 'Must be {number} characters length',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||
verify_id: 'Verify ID',
|
||||
identity_verification: 'Identity verification',
|
||||
@@ -98,6 +99,7 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||
verify_id: 'Verificar ID',
|
||||
identity_verification: 'Verificación de identidad',
|
||||
@@ -197,6 +199,7 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||
verify_id: 'Verifica ID',
|
||||
identity_verification: "Verifica dell'identità",
|
||||
@@ -296,6 +299,7 @@ const it = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
||||
verify_id: 'ID überprüfen',
|
||||
identity_verification: 'Identitätsüberprüfung',
|
||||
@@ -395,6 +399,7 @@ const de = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
must_be_characters_length: 'Doit contenir {number} caractères',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
||||
verify_id: "Vérification de l'ID",
|
||||
identity_verification: "Vérification de l'identité",
|
||||
@@ -494,6 +499,7 @@ const fr = {
|
||||
}
|
||||
|
||||
const pl = {
|
||||
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||
verify_id: 'Zweryfikuj ID',
|
||||
identity_verification: 'Weryfikacja tożsamości',
|
||||
@@ -593,6 +599,7 @@ const pl = {
|
||||
}
|
||||
|
||||
const uk = {
|
||||
must_be_characters_length: 'Має містити {number} символів',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||
verify_id: 'Підтвердження ідентичності',
|
||||
identity_verification: 'Ідентифікація особи',
|
||||
@@ -692,6 +699,7 @@ const uk = {
|
||||
}
|
||||
|
||||
const cs = {
|
||||
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||
verify_id: 'Ověřit ID',
|
||||
identity_verification: 'Ověření identity',
|
||||
@@ -791,6 +799,7 @@ const cs = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||
verify_id: 'Verificar ID',
|
||||
identity_verification: 'Verificação de identidade',
|
||||
@@ -890,6 +899,7 @@ const pt = {
|
||||
}
|
||||
|
||||
const he = {
|
||||
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||
verify_id: 'אמת מזהה',
|
||||
identity_verification: 'אימות זהות',
|
||||
@@ -989,6 +999,7 @@ const he = {
|
||||
}
|
||||
|
||||
const nl = {
|
||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||
verify_id: 'Verifiëren ID',
|
||||
identity_verification: 'Identiteitsverificatie',
|
||||
@@ -1088,6 +1099,7 @@ const nl = {
|
||||
}
|
||||
|
||||
const ar = {
|
||||
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||
verify_id: 'تحقق من الهوية',
|
||||
identity_verification: 'التحقق من الهوية',
|
||||
@@ -1187,6 +1199,7 @@ const ar = {
|
||||
}
|
||||
|
||||
const ko = {
|
||||
must_be_characters_length: '{number}자여야 합니다',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||
verify_id: '아이디 확인',
|
||||
identity_verification: '신원 확인',
|
||||
@@ -1286,6 +1299,7 @@ const ko = {
|
||||
}
|
||||
|
||||
const ja = {
|
||||
must_be_characters_length: '{number}文字でなければなりません',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||
verify_id: '本人確認',
|
||||
identity_verification: '本人確認',
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
type="text"
|
||||
:name="`values[${field.uuid}]`"
|
||||
@invalid="field.validation?.message ? $event.target.setCustomValidity(field.validation.message) : ''"
|
||||
@input="field.validation?.message ? $event.target.setCustomValidity('') : ''"
|
||||
@invalid="validationMessage ? $event.target.setCustomValidity(validationMessage) : ''"
|
||||
@input="validationMessage ? $event.target.setCustomValidity('') : ''"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
<textarea
|
||||
@@ -127,6 +127,28 @@ export default {
|
||||
return null
|
||||
}
|
||||
},
|
||||
lengthValidation () {
|
||||
if (this.field.validation?.pattern) {
|
||||
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
validationMessage () {
|
||||
if (this.field.validation?.message) {
|
||||
return this.field.validation.message
|
||||
} else if (this.lengthValidation) {
|
||||
const number =
|
||||
[this.lengthValidation.min, this.lengthValidation.max]
|
||||
.filter(Boolean)
|
||||
.filter((v, i, a) => a.indexOf(v) === i)
|
||||
.join('-')
|
||||
|
||||
return this.t('must_be_characters_length').replace('{number}', number)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
text: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
|
||||
@@ -153,13 +153,13 @@
|
||||
<option
|
||||
v-for="(key, value) in validations"
|
||||
:key="key"
|
||||
:selected="field.validation?.pattern ? value === field.validation.pattern : value === 'none'"
|
||||
:value="value"
|
||||
:selected="lengthValidation ? key == 'length' : (field.validation?.pattern ? value === field.validation.pattern : key === 'none')"
|
||||
:value="key"
|
||||
>
|
||||
{{ t(key) }}
|
||||
</option>
|
||||
<option
|
||||
:selected="field.validation && !validations[field.validation.pattern]"
|
||||
:selected="field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||
:value="validations[field.validation?.pattern] || !field.validation?.pattern ? 'custom' : field.validation?.pattern"
|
||||
>
|
||||
{{ t('custom') }}
|
||||
@@ -174,7 +174,51 @@
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern]"
|
||||
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
|
||||
class="py-1.5 px-1 relative flex space-x-1"
|
||||
@click.stop
|
||||
>
|
||||
<div class="w-1/2 relative">
|
||||
<input
|
||||
:placeholder="t('min')"
|
||||
type="number"
|
||||
min="0"
|
||||
:value="lengthValidation.min"
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
|
||||
@blur="save"
|
||||
>
|
||||
<label
|
||||
v-if="lengthValidation.min"
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('min') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-1/2 relative">
|
||||
<input
|
||||
:placeholder="t('max')"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
:value="lengthValidation.max"
|
||||
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
|
||||
@blur="save"
|
||||
>
|
||||
<label
|
||||
v-if="lengthValidation.max"
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('max') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
@@ -195,6 +239,27 @@
|
||||
{{ t('regexp_validation') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
v-model="field.validation.message"
|
||||
:placeholder="t('error_message')"
|
||||
dir="auto"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||
@blur="save"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation.message"
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('error_message') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="field.type === 'date'"
|
||||
class="py-1.5 px-1 relative"
|
||||
@@ -330,7 +395,7 @@
|
||||
v-if="field.type != 'stamp'"
|
||||
class="pb-0.5 mt-0.5"
|
||||
>
|
||||
<li v-if="['text', 'number', 'date'].includes(field.type)">
|
||||
<li v-if="['text', 'number', 'date', 'select'].includes(field.type)">
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@click="$emit('click-font')"
|
||||
@@ -489,6 +554,7 @@ export default {
|
||||
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
|
||||
data () {
|
||||
return {
|
||||
selectedValidation: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -533,8 +599,16 @@ export default {
|
||||
|
||||
return formats
|
||||
},
|
||||
lengthValidation () {
|
||||
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
||||
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
'.{0,}': 'length',
|
||||
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'ssn',
|
||||
'^[0-9]{2}-[0-9]{7}$': 'ein',
|
||||
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'email',
|
||||
@@ -553,13 +627,21 @@ export default {
|
||||
methods: {
|
||||
onChangeValidation (event) {
|
||||
if (event.target.value === 'custom') {
|
||||
this.field.validation = { pattern: '' }
|
||||
this.selectedValidation = 'custom'
|
||||
|
||||
this.field.validation = { pattern: '', message: '' }
|
||||
|
||||
this.$nextTick(() => this.$refs.validationCustom.focus())
|
||||
} else if (event.target.value) {
|
||||
this.field.validation ||= {}
|
||||
this.field.validation.pattern = event.target.value
|
||||
this.field.validation.pattern =
|
||||
Object.keys(this.validations).find(key => this.validations[key] === event.target.value)
|
||||
|
||||
this.selectedValidation = event.target.value
|
||||
delete this.field.validation.message
|
||||
} else {
|
||||
this.selectedValidation = ''
|
||||
|
||||
delete this.field.validation
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const en = {
|
||||
error_message: 'Error message',
|
||||
length: 'Length',
|
||||
min: 'Min',
|
||||
max: 'Max',
|
||||
font: 'Font',
|
||||
party: 'Party',
|
||||
date_signed: 'Date Signed',
|
||||
@@ -168,6 +172,10 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
error_message: 'Mensaje de error',
|
||||
length: 'Longitud',
|
||||
min: 'Mín',
|
||||
max: 'Máx',
|
||||
date_signed: 'Fecha actual',
|
||||
fuente: 'Fuente',
|
||||
party: 'Parte',
|
||||
@@ -337,6 +345,10 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
error_message: 'Messaggio di errore',
|
||||
length: 'Lunghezza',
|
||||
min: 'Min',
|
||||
max: 'Max',
|
||||
date_signed: 'Data attuale',
|
||||
font: 'Carattere',
|
||||
party: 'Parte',
|
||||
@@ -506,6 +518,10 @@ const it = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
error_message: 'Mensagem de erro',
|
||||
length: 'Comprimento',
|
||||
min: 'Mín',
|
||||
max: 'Máx',
|
||||
date_signed: 'Data atual',
|
||||
fonte: 'Fonte',
|
||||
party: 'Parte',
|
||||
@@ -675,6 +691,10 @@ const pt = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
error_message: 'Message d\'erreur',
|
||||
length: 'Longueur',
|
||||
min: 'Min',
|
||||
max: 'Max',
|
||||
date_signed: 'Date actuelle',
|
||||
font: 'Police',
|
||||
party: 'Partie',
|
||||
@@ -844,6 +864,10 @@ const fr = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
error_message: 'Fehlermeldung',
|
||||
length: 'Länge',
|
||||
min: 'Min',
|
||||
max: 'Max',
|
||||
date_now: 'Akt. Datum',
|
||||
font: 'Schriftart',
|
||||
party: 'Partei',
|
||||
|
||||
@@ -11,9 +11,6 @@ class ProcessSubmissionExpiredJob
|
||||
return if submission.submitters.where.not(declined_at: nil).exists?
|
||||
return unless submission.submitters.exists?(completed_at: nil)
|
||||
|
||||
WebhookUrls.for_account_id(submission.account_id, %w[submission.expired]).each do |webhook|
|
||||
SendSubmissionExpiredWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(submission, 'submission.expired')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -63,16 +63,24 @@ class ProcessSubmitterCompletionJob
|
||||
end
|
||||
|
||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||
event_uuids = {}
|
||||
|
||||
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
|
||||
if webhook.events.include?('form.completed')
|
||||
event_uuids['form.completed'] ||= SecureRandom.uuid
|
||||
|
||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'event_uuid' => event_uuids['form.completed'],
|
||||
'webhook_url_id' => webhook.id)
|
||||
end
|
||||
|
||||
if webhook.events.include?('submission.completed') && is_all_completed
|
||||
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook.id)
|
||||
end
|
||||
next unless webhook.events.include?('submission.completed') && is_all_completed
|
||||
|
||||
event_uuids['submission.completed'] ||= SecureRandom.uuid
|
||||
|
||||
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'event_uuid' => event_uuids['submission.completed'],
|
||||
'webhook_url_id' => webhook.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ class SendFormCompletedWebhookRequestJob
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submitter,
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
|
||||
@@ -18,13 +18,15 @@ class SendFormDeclinedWebhookRequestJob
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.declined',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submitter,
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
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,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -18,13 +18,15 @@ class SendFormStartedWebhookRequestJob
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.started',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submitter,
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
'submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -18,13 +18,15 @@ class SendFormViewedWebhookRequestJob
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.viewed',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submitter,
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
'submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -16,13 +16,15 @@ class SendSubmissionArchivedWebhookRequestJob
|
||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.archived',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submission,
|
||||
attempt:,
|
||||
data: submission.as_json(only: %i[id archived_at]))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
'submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -16,6 +16,9 @@ class SendSubmissionCompletedWebhookRequestJob
|
||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.completed',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submission,
|
||||
attempt:,
|
||||
data: Submissions::SerializeForApi.call(submission))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
|
||||
@@ -16,13 +16,15 @@ class SendSubmissionCreatedWebhookRequestJob
|
||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.created',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submission,
|
||||
attempt:,
|
||||
data: Submissions::SerializeForApi.call(submission))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
'submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -16,13 +16,15 @@ class SendSubmissionExpiredWebhookRequestJob
|
||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.expired')
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.expired',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: submission,
|
||||
attempt:,
|
||||
data: Submissions::SerializeForApi.call(submission))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
'submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -16,13 +16,15 @@ class SendTemplateCreatedWebhookRequestJob
|
||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.created',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: template,
|
||||
attempt:,
|
||||
data: Templates::SerializeForApi.call(template))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
'template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -16,13 +16,15 @@ class SendTemplateUpdatedWebhookRequestJob
|
||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
|
||||
|
||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.updated',
|
||||
event_uuid: params['event_uuid'],
|
||||
record: template,
|
||||
attempt:,
|
||||
data: Templates::SerializeForApi.call(template))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
'template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id,
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SendTestWebhookRequestJob
|
||||
include Sidekiq::Job
|
||||
|
||||
sidekiq_options retry: 0
|
||||
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
def perform(params = {})
|
||||
submitter = Submitter.find(params['submitter_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
return unless webhook_url && submitter
|
||||
|
||||
Faraday.post(webhook_url.url,
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: Time.current.iso8601,
|
||||
data: Submitters::SerializeForWebhook.call(submitter)
|
||||
}.to_json,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => USER_AGENT,
|
||||
**webhook_url.secret.to_h)
|
||||
end
|
||||
end
|
||||
@@ -33,6 +33,7 @@ class Account < ApplicationRecord
|
||||
has_many :account_linked_accounts, dependent: :destroy
|
||||
has_many :email_events, dependent: :destroy
|
||||
has_many :webhook_urls, dependent: :destroy
|
||||
has_many :webhook_events, dependent: nil
|
||||
has_many :account_accesses, dependent: :destroy
|
||||
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
||||
class_name: 'AccountLinkedAccount',
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: webhook_attempts
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# attempt :integer not null
|
||||
# response_body :text
|
||||
# response_status_code :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# webhook_event_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_webhook_attempts_on_webhook_event_id (webhook_event_id)
|
||||
#
|
||||
class WebhookAttempt < ApplicationRecord
|
||||
belongs_to :webhook_event
|
||||
|
||||
def success?
|
||||
[2, 3].include?(response_status_code.to_i / 100)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: webhook_events
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# event_type :string not null
|
||||
# record_type :string not null
|
||||
# status :string not null
|
||||
# uuid :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# record_id :bigint not null
|
||||
# webhook_url_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_webhook_events_error (webhook_url_id,id) WHERE ((status)::text = 'error'::text)
|
||||
# index_webhook_events_on_uuid_and_webhook_url_id (uuid,webhook_url_id) UNIQUE
|
||||
# index_webhook_events_on_webhook_url_id_and_id (webhook_url_id,id)
|
||||
#
|
||||
class WebhookEvent < ApplicationRecord
|
||||
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||
|
||||
belongs_to :webhook_url, optional: true
|
||||
belongs_to :account, optional: true
|
||||
belongs_to :record, polymorphic: true, optional: true
|
||||
|
||||
has_many :webhook_attempts, dependent: nil
|
||||
end
|
||||
@@ -37,6 +37,7 @@ class WebhookUrl < ApplicationRecord
|
||||
].freeze
|
||||
|
||||
belongs_to :account
|
||||
has_many :webhook_events, dependent: nil
|
||||
|
||||
attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] }
|
||||
attribute :secret, :string, default: -> { {} }
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
|
||||
}
|
||||
|
||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||
this.field.classList.add('text-[0.7vw]', 'lg:text-[0.45rem]');
|
||||
}
|
||||
}
|
||||
}
|
||||
get field() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<% dashboard_templates_order = cookies.permanent[:dashboard_templates_order] || 'created_at' %>
|
||||
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
|
||||
<label tabindex="0" class="btn btn-sm h-10">
|
||||
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
|
||||
@@ -6,7 +5,7 @@
|
||||
<ul tabindex="0" class="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box mb-1 min-w-48">
|
||||
<toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
|
||||
<li>
|
||||
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'created_at' %>">
|
||||
<button class="<%= 'bg-base-200' if selected_order == 'created_at' %>">
|
||||
<%= svg_icon('sort_descending_numbers', class: 'w-4 h-4') %>
|
||||
<span class="whitespace-nowrap"><%= t('newest_first') %></span>
|
||||
</button>
|
||||
@@ -15,7 +14,7 @@
|
||||
<% if local_assigns[:with_recently_used] != false %>
|
||||
<toggle-cookies data-value="used_at" data-key="dashboard_templates_order">
|
||||
<li>
|
||||
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'used_at' %>">
|
||||
<button class="<%= 'bg-base-200' if selected_order == 'used_at' %>">
|
||||
<%= svg_icon('sort_descending_small_big', class: 'w-4 h-4') %>
|
||||
<span class="whitespace-nowrap"><%= t('recently_used') %></span>
|
||||
</button>
|
||||
@@ -24,7 +23,7 @@
|
||||
<% end %>
|
||||
<toggle-cookies data-value="name" data-key="dashboard_templates_order">
|
||||
<li>
|
||||
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'name' %>">
|
||||
<button class="<%= 'bg-base-200' if selected_order == 'name' %>">
|
||||
<%= svg_icon('sort_ascending_letters', class: 'w-4 h-4') %>
|
||||
<span class="whitespace-nowrap"><%= t('name_a_z') %></span>
|
||||
</button>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
×
|
||||
</span>
|
||||
<% end %>
|
||||
<div class="w-full md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
|
||||
<div class="w-screen md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
|
||||
<%= yield %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<% templates_order_select_html = capture do %>
|
||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
|
||||
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<% end %>
|
||||
<% templates_order_select_html = capture do %>
|
||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
|
||||
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @template_folders.present? %>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
|
||||
<% else %>
|
||||
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
|
||||
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] != false ? [t('submission_requester'), 'is_requester'] : nil), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %>
|
||||
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %>
|
||||
</toggle-attribute>
|
||||
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<div id="drawer_events_<%= dom_id(webhook_event) %>">
|
||||
<ol class="relative border-s border-base-300 space-y-6 ml-3">
|
||||
<% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %>
|
||||
<% if webhook_event.status == 'error' %>
|
||||
<% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %>
|
||||
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 %>
|
||||
<li class="ml-7">
|
||||
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;">
|
||||
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
<p class="leading-none text-base-content/90 pt-1">
|
||||
<%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, last_attempt.created_at + (2**last_attempt.attempt).minutes)) %>
|
||||
</p>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if webhook_attempts.present? %>
|
||||
<% webhook_attempts.each do |webhook_attempt| %>
|
||||
<li class="ml-7">
|
||||
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center <%= webhook_attempt.success? ? 'btn-success bg-lime-50' : 'btn-error bg-red-50' %>" style="left: -12px;">
|
||||
<%= svg_icon(webhook_attempt.success? ? 'check' : 'x', class: 'w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
<p class="leading-none text-sm text-base-content/60 pt-1">
|
||||
<%= l(webhook_attempt.created_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm font-bold text-base-content/80">
|
||||
<span><%= Rack::Utils::HTTP_STATUS_CODES[webhook_attempt.response_status_code] %></span>
|
||||
<% if webhook_attempt.response_status_code.positive? %>
|
||||
<span>(<%= webhook_attempt.response_status_code %>)</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% unless webhook_attempt.success? %>
|
||||
<p class="text-sm text-base-content/80 mt-1">
|
||||
<%= webhook_attempt.response_body.presence || Rack::Utils::HTTP_STATUS_CODES[webhook_attempt.response_status_code] %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<li class="ml-7">
|
||||
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center btn-info bg-blue-50" style="left: -12px;">
|
||||
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
<p class="leading-none text-base-content/60 pt-1">
|
||||
<%= l(webhook_event.created_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||
</p>
|
||||
</li>
|
||||
<% end %>
|
||||
</ol>
|
||||
<% unless webhook_event.status == 'pending' %>
|
||||
<div class="absolute right-4 top-3">
|
||||
<%= button_to button_title(title: t('resend'), disabled_with: t('awaiting'), icon: svg_icon('rotate', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), resend_settings_webhook_event_path(webhook_url.id, webhook_event.uuid), form: { id: button_uuid = SecureRandom.uuid }, params: { button_id: button_uuid }, class: 'btn btn-neutral btn-sm text-white', method: :post, onclick: '[this.form.requestSubmit(), this.disabled = true]' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div id="<%= dom_id(webhook_event) %>" class="group relative hover:cursor-pointer hover:bg-base-200">
|
||||
<a href="<%= settings_webhook_event_path(webhook_url.id, webhook_event.uuid) %>" data-turbo-frame="drawer" class="top-0 bottom-0 left-0 right-0 absolute"></a>
|
||||
<div class="min-h-12 flex flex-col md:flex-row md:items-center md:justify-between gap-2 px-3 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<% if webhook_event.status == 'success' %>
|
||||
<div class="btn btn-outline btn-xs btn-success bg-lime-50 gap-1">
|
||||
<%= svg_icon('check', class: 'w-4 h-4 shrink-0 stroke-2') %>
|
||||
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
|
||||
</div>
|
||||
<% elsif webhook_event.status == 'pending' %>
|
||||
<div class="btn btn-outline btn-xs btn-info bg-blue-50 gap-1">
|
||||
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0 stroke-2') %>
|
||||
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
|
||||
</div>
|
||||
<% elsif webhook_event.status == 'error' %>
|
||||
<div class="btn btn-outline btn-xs btn-error bg-red-50 gap-1">
|
||||
<%= svg_icon('x', class: 'w-4 h-4 shrink-0') %>
|
||||
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div><%= webhook_event.event_type %></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<%= button_to button_title(title: t('resend'), disabled_with: t('awaiting'), icon: svg_icon('rotate', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), resend_settings_webhook_event_path(webhook_url.id, webhook_event.uuid), form: { id: button_uuid = SecureRandom.uuid }, params: { button_id: button_uuid }, class: 'btn btn-neutral btn-xs h-2 text-white relative z-[1] hidden md:group-hover:inline-block', data: { turbo_frame: :drawer }, method: :post, onclick: "[this.form.requestSubmit(), this.disabled = true, this.classList.remove('hidden')]" %>
|
||||
<span><%= l(webhook_event.created_at, locale: current_account.locale, format: :short) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<%= render 'shared/turbo_drawer', title: @webhook_event.event_type, close_after_submit: false do %>
|
||||
<div class="relative px-4 py-4">
|
||||
<%= render 'drawer_events', webhook_url: @webhook_url, webhook_event: @webhook_event %>
|
||||
<% if @data %>
|
||||
<div class="mockup-code overflow-hidden relative pb-0 mt-6">
|
||||
<% response = JSON.pretty_generate({ event_type: @webhook_event.event_type, timestamp: @webhook_event.created_at.as_json, data: @data }) %>
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: response, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
</span>
|
||||
<pre class="before:!m-0 pl-4 pb-4"><code class="overflow-hidden text-sm w-full"><%== HighlightCode.call(response, 'JSON', theme: 'base16.dark') %></code></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -79,8 +79,49 @@
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
|
||||
<% if submitter %>
|
||||
<% if @webhook_events.present? || params[:status].present? %>
|
||||
<div class="mt-6">
|
||||
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2>
|
||||
<div class="tabs border-b mt-4">
|
||||
<%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
<%= link_to t('successed'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
<%= link_to t('failed'), url_for(params.to_unsafe_h.merge(status: 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
</div>
|
||||
<% if @webhook_events.present? %>
|
||||
<div class="divide-y divide-base-300 rounded-lg">
|
||||
<%= render partial: 'webhook_events/event_row', collection: @webhook_events, as: :webhook_event, locals: { webhook_url: @webhook_url } %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-4">
|
||||
<%= t('there_are_no_events') %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @pagy.pages > 1 %>
|
||||
<div class="flex my-4 justify-center md:justify-between">
|
||||
<div class="hidden md:block text-sm">
|
||||
<%= "#{@pagy.from}-#{@pagy.to} events" %>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<div class="join">
|
||||
<% if @pagy.prev %>
|
||||
<%= link_to '«', url_for(page: @pagy.prev, anchor: 'log'), class: 'join-item btn min-h-full h-10' %>
|
||||
<% else %>
|
||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
||||
<% end %>
|
||||
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
||||
<%= "Page #{@pagy.page}" %>
|
||||
</span>
|
||||
<% if @pagy.next %>
|
||||
<%= link_to '»', url_for(page: @pagy.next, anchor: 'log'), class: 'join-item btn min-h-full h-10' %>
|
||||
<% else %>
|
||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif (submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last) && can?(:read, submitter) %>
|
||||
<div class="space-y-4 mt-4">
|
||||
<div class="collapse collapse-open bg-base-200 px-1">
|
||||
<div class="p-4 text-xl font-medium">
|
||||
@@ -98,7 +139,7 @@
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.completed', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
</span>
|
||||
<pre><code class="overflow-hidden w-full"><%= code %></code></pre>
|
||||
<pre><code class="overflow-hidden w-full"><%== HighlightCode.call(code, 'JSON', theme: 'base16.dark') %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ignored_warnings": [
|
||||
{
|
||||
"fingerprint": "25f4ce5fee1e1180fa1919dc4ee78db3ab3457a956e4679503aa745771a43836",
|
||||
"fingerprint": "bbd1bdad94998e53a48921859065e06cd1595502d4dc40362afcaa90307b591a",
|
||||
"note": "Permitted parameters are necessary for creating submitters via API"
|
||||
},
|
||||
{
|
||||
@@ -19,6 +19,10 @@
|
||||
{
|
||||
"fingerprint": "5f52190d03ee922bba9792012d8fcbeb7d4736006bb899b3be9cc10d679e0af1",
|
||||
"note": "Safe Param"
|
||||
},
|
||||
{
|
||||
"fingerprint": "dbbfb4a4ace7f43d8247cbb44afa8b628e005e6194ca5552e029b200f725a2d5",
|
||||
"message": "Unescaped find_by!(uuid: params[:id]) is not risky"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
autoload :InheritableHash, 'rouge/util'
|
||||
autoload :Token, 'rouge/token'
|
||||
autoload :Lexer, 'rouge/lexer'
|
||||
autoload :RegexLexer, 'rouge/regex_lexer'
|
||||
|
||||
module Lexers
|
||||
autoload :JSON, 'rouge/lexers/json'
|
||||
end
|
||||
|
||||
autoload :Formatter, 'rouge/formatter'
|
||||
|
||||
module Formatters
|
||||
autoload :HTML, 'rouge/formatters/html'
|
||||
autoload :HTMLInline, 'rouge/formatters/html_inline'
|
||||
end
|
||||
|
||||
autoload :Theme, 'rouge/theme'
|
||||
end
|
||||
@@ -763,6 +763,12 @@ en: &en
|
||||
link_form_fields: Link form fields
|
||||
at_least_one_field_must_be_displayed_in_the_form: At least one field must be displayed in the form.
|
||||
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: This template has multiple parties, which prevents the use of a shared link as it's unclear which party is responsible for specific fields. To resolve this, define the default party details.
|
||||
events_log: Events Log
|
||||
successed: Successed
|
||||
failed: Failed
|
||||
there_are_no_events: There are no events
|
||||
resend: Resend
|
||||
next_attempt_in_time_in_words: Next attempt in %{time_in_words}
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Bulk Send
|
||||
@@ -863,6 +869,9 @@ en: &en
|
||||
items:
|
||||
range_with_total: "%{from}-%{to} of %{count} items"
|
||||
range_without_total: "%{from}-%{to} items"
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} of %{count} events"
|
||||
range_without_total: "%{from}-%{to} events"
|
||||
|
||||
es: &es
|
||||
default_parties: Partes predeterminadas
|
||||
@@ -1609,6 +1618,12 @@ es: &es
|
||||
link_form_fields: Vincular campos del formulario
|
||||
at_least_one_field_must_be_displayed_in_the_form: Al menos un campo debe mostrarse en el formulario.
|
||||
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Esta plantilla tiene varias partes, lo que impide el uso de un enlace compartido porque no está claro qué parte es responsable de campos específicos. Para resolverlo, define los detalles predeterminados de la parte.
|
||||
events_log: Registro de eventos
|
||||
successed: Exitoso
|
||||
failed: Fallido
|
||||
there_are_no_events: No hay eventos
|
||||
resend: Reenviar
|
||||
next_attempt_in_time_in_words: Próximo intento en %{time_in_words}
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envío masivo
|
||||
@@ -1709,6 +1724,9 @@ es: &es
|
||||
items:
|
||||
range_with_total: "%{from}-%{to} de %{count} elementos"
|
||||
range_without_total: "%{from}-%{to} elementos"
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} de %{count} eventos"
|
||||
range_without_total: "%{from}-%{to} eventos"
|
||||
|
||||
it: &it
|
||||
default_parties: Parti predefiniti
|
||||
@@ -2453,6 +2471,12 @@ it: &it
|
||||
link_form_fields: Collega i campi del modulo
|
||||
at_least_one_field_must_be_displayed_in_the_form: Almeno un campo deve essere visualizzato nel modulo.
|
||||
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Questo modello ha più parti, il che impedisce l’uso di un link di condivisione perché non è chiaro quale parte sia responsabile di campi specifici. Per risolvere, definisci i dettagli predefiniti della parte.
|
||||
events_log: Registro eventi
|
||||
successed: Riuscito
|
||||
failed: Fallito
|
||||
there_are_no_events: Nessun evento
|
||||
resend: Invia di nuovo
|
||||
next_attempt_in_time_in_words: Prossimo tentativo tra %{time_in_words}
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Invio massivo
|
||||
@@ -2553,6 +2577,9 @@ it: &it
|
||||
items:
|
||||
range_with_total: "%{from}-%{to} di %{count} elementi"
|
||||
range_without_total: "%{from}-%{to} elementi"
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} di %{count} eventi"
|
||||
range_without_total: "%{from}-%{to} eventi"
|
||||
|
||||
fr: &fr
|
||||
default_parties: Parties par défaut
|
||||
@@ -3300,6 +3327,12 @@ fr: &fr
|
||||
link_form_fields: Lier les champs du formulaire
|
||||
at_least_one_field_must_be_displayed_in_the_form: Au moins un champ doit être affiché dans le formulaire.
|
||||
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Ce modèle contient plusieurs parties, ce qui empêche l’utilisation d’un lien de partage car il n’est pas clair quelle partie est responsable de certains champs. Pour résoudre cela, définissez les détails de la partie par défaut.
|
||||
events_log: Journal des événements
|
||||
successed: Réussi
|
||||
failed: Échoué
|
||||
there_are_no_events: Aucun événement
|
||||
resend: Renvoyer
|
||||
next_attempt_in_time_in_words: Nouvelle tentative dans %{time_in_words}
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envoi en masse
|
||||
@@ -3400,6 +3433,9 @@ fr: &fr
|
||||
items:
|
||||
range_with_total: "%{from} à %{to} sur %{count} éléments"
|
||||
range_without_total: "%{from} à %{to} éléments"
|
||||
events:
|
||||
range_with_total: "%{from} à %{to} sur %{count} événements"
|
||||
range_without_total: "%{from} à %{to} événements"
|
||||
|
||||
pt: &pt
|
||||
default_parties: Partes padrão
|
||||
@@ -4146,6 +4182,12 @@ pt: &pt
|
||||
link_form_fields: Vincular campos do formulário
|
||||
at_least_one_field_must_be_displayed_in_the_form: Pelo menos um campo deve ser exibido no formulário.
|
||||
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Este modelo tem várias partes, o que impede o uso de um link de compartilhamento, pois não está claro qual parte é responsável por campos específicos. Para resolver isso, defina os detalhes padrão da parte.
|
||||
events_log: Registro de eventos
|
||||
successed: Sucesso
|
||||
failed: Falhou
|
||||
there_are_no_events: Nenhum evento
|
||||
resend: Reenviar
|
||||
next_attempt_in_time_in_words: Próxima tentativa em %{time_in_words}
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envio em massa
|
||||
@@ -4247,6 +4289,9 @@ pt: &pt
|
||||
items:
|
||||
range_with_total: "%{from}-%{to} de %{count} itens"
|
||||
range_without_total: "%{from}-%{to} itens"
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} de %{count} eventos"
|
||||
range_without_total: "%{from}-%{to} eventos"
|
||||
|
||||
de: &de
|
||||
default_parties: Standardparteien
|
||||
@@ -4993,6 +5038,12 @@ de: &de
|
||||
link_form_fields: Formularfelder verknüpfen
|
||||
at_least_one_field_must_be_displayed_in_the_form: Mindestens ein Feld muss im Formular angezeigt werden.
|
||||
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Diese Vorlage enthält mehrere Parteien, was die Verwendung eines Freigabelinks verhindert, da unklar ist, welche Partei für bestimmte Felder verantwortlich ist. Um dies zu beheben, definieren Sie die Standardparteidetails.
|
||||
events_log: Ereignisprotokoll
|
||||
successed: Erfolgreich
|
||||
failed: Fehlgeschlagen
|
||||
there_are_no_events: Keine Ereignisse vorhanden
|
||||
resend: Erneut senden
|
||||
next_attempt_in_time_in_words: Nächster Versuch in %{time_in_words}
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Massenversand
|
||||
@@ -5093,6 +5144,9 @@ de: &de
|
||||
items:
|
||||
range_with_total: "%{from}-%{to} von %{count} Elementen"
|
||||
range_without_total: "%{from}-%{to} Elemente"
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} von %{count} Ereignissen"
|
||||
range_without_total: "%{from}-%{to} Ereignisse"
|
||||
|
||||
pl:
|
||||
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
|
||||
|
||||
@@ -179,6 +179,11 @@ Rails.application.routes.draw do
|
||||
resources :api, only: %i[index create], controller: 'api_settings'
|
||||
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
||||
post :resend
|
||||
|
||||
resources :events, only: %i[show], controller: 'webhook_events' do
|
||||
post :resend, on: :member
|
||||
post :refresh, on: :member
|
||||
end
|
||||
end
|
||||
resource :account, only: %i[show update destroy]
|
||||
resources :profile, only: %i[index] do
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateWebhookEventsAndAttempts < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :webhook_events do |t|
|
||||
t.string :uuid, null: false
|
||||
t.bigint :webhook_url_id, null: false
|
||||
t.bigint :account_id, null: false
|
||||
t.bigint :record_id, null: false
|
||||
t.string :record_type, null: false
|
||||
t.string :event_type, null: false
|
||||
t.string :status, null: false
|
||||
|
||||
t.index %i[uuid webhook_url_id], unique: true
|
||||
t.index %i[webhook_url_id id]
|
||||
t.index %i[webhook_url_id id], where: "status = 'error'", name: 'index_webhook_events_error'
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :webhook_attempts do |t|
|
||||
t.bigint :webhook_event_id, null: false, index: true
|
||||
t.text :response_body
|
||||
t.integer :response_status_code, null: false
|
||||
t.integer :attempt, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
+26
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_18_085322) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "btree_gin"
|
||||
enable_extension "plpgsql"
|
||||
@@ -437,6 +437,31 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_18_085322) do
|
||||
t.index ["uuid"], name: "index_users_on_uuid", unique: true
|
||||
end
|
||||
|
||||
create_table "webhook_attempts", force: :cascade do |t|
|
||||
t.bigint "webhook_event_id", null: false
|
||||
t.text "response_body"
|
||||
t.integer "response_status_code", null: false
|
||||
t.integer "attempt", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["webhook_event_id"], name: "index_webhook_attempts_on_webhook_event_id"
|
||||
end
|
||||
|
||||
create_table "webhook_events", force: :cascade do |t|
|
||||
t.string "uuid", null: false
|
||||
t.bigint "webhook_url_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.string "record_type", null: false
|
||||
t.string "event_type", null: false
|
||||
t.string "status", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["uuid", "webhook_url_id"], name: "index_webhook_events_on_uuid_and_webhook_url_id", unique: true
|
||||
t.index ["webhook_url_id", "id"], name: "index_webhook_events_error", where: "((status)::text = 'error'::text)"
|
||||
t.index ["webhook_url_id", "id"], name: "index_webhook_events_on_webhook_url_id_and_id"
|
||||
end
|
||||
|
||||
create_table "webhook_urls", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.text "url", null: false
|
||||
|
||||
@@ -13,7 +13,7 @@ module Abilities
|
||||
TemplateSharing.where({ ability:, account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
|
||||
.select(:template_id)
|
||||
|
||||
Template.where(Template.arel_table[:id].in(templates.select(:id).arel.union(shared_ids.arel)))
|
||||
Template.where(Template.arel_table[:id].in(templates.select(:id).arel.union(:all, shared_ids.arel)))
|
||||
end
|
||||
|
||||
def entity(template, user:, ability: nil)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HighlightCode
|
||||
module_function
|
||||
|
||||
def call(code, lexer, theme: 'base16.light')
|
||||
require 'rouge/themes/base16' unless Rouge::Theme.registry[theme]
|
||||
|
||||
formatter = Rouge::Formatters::HTMLInline.new(theme)
|
||||
lexer = Rouge::Lexers.const_get(lexer.to_sym).new
|
||||
formatted_code = formatter.format(lexer.lex(code))
|
||||
formatted_code = formatted_code.gsub('background-color: #181818', '') if theme == 'base16.dark'
|
||||
formatted_code
|
||||
end
|
||||
end
|
||||
+4
-2
@@ -4,8 +4,10 @@ module ImageUtils
|
||||
module_function
|
||||
|
||||
def blank?(image)
|
||||
min = (0...image.bands).map { |i| image.stats.getpoint(0, i)[0] }
|
||||
max = (0...image.bands).map { |i| image.stats.getpoint(1, i)[0] }
|
||||
stats = image.stats
|
||||
|
||||
min = (0...image.bands).map { |i| stats.getpoint(0, i)[0] }
|
||||
max = (0...image.bands).map { |i| stats.getpoint(1, i)[0] }
|
||||
|
||||
return true if min.all?(255) && max.all?(255)
|
||||
return true if min.all?(0) && max.all?(0)
|
||||
|
||||
@@ -56,7 +56,7 @@ module SearchEntries
|
||||
end
|
||||
|
||||
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
|
||||
elsif keyword.match?(/[^\p{L}\d&@.\-]/) || keyword.match?(/\A['"].*['"]\z/)
|
||||
elsif keyword.match?(/[^\p{L}\d&@.\-]/) || keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.\-]{2,}/)
|
||||
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
|
||||
else
|
||||
keyword = TextUtils.transliterate(keyword.downcase).squish
|
||||
|
||||
@@ -5,12 +5,16 @@ module SendWebhookRequest
|
||||
|
||||
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
|
||||
|
||||
MANUAL_ATTEMPT = 99_999
|
||||
AUTOMATED_RETRY_RANGE = 1..MANUAL_ATTEMPT - 1
|
||||
|
||||
HttpsError = Class.new(StandardError)
|
||||
LocalhostError = Class.new(StandardError)
|
||||
|
||||
module_function
|
||||
|
||||
def call(webhook_url, event_type:, data:)
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def call(webhook_url, event_uuid:, event_type:, record:, data:, attempt: 0)
|
||||
uri = begin
|
||||
URI(webhook_url.url)
|
||||
rescue URI::Error
|
||||
@@ -24,21 +28,81 @@ module SendWebhookRequest
|
||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)
|
||||
end
|
||||
|
||||
Faraday.post(uri) do |req|
|
||||
webhook_event = create_webhook_event(webhook_url, event_uuid:, event_type:, record:)
|
||||
|
||||
return if AUTOMATED_RETRY_RANGE.cover?(attempt.to_i) && webhook_event&.status == 'success'
|
||||
|
||||
response = Faraday.post(uri) do |req|
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.headers['User-Agent'] = USER_AGENT
|
||||
req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present?
|
||||
|
||||
req.body = {
|
||||
event_type: event_type,
|
||||
timestamp: Time.current,
|
||||
timestamp: webhook_event&.created_at || Time.current,
|
||||
data: data
|
||||
}.to_json
|
||||
|
||||
req.options.read_timeout = 8
|
||||
req.options.open_timeout = 8
|
||||
end
|
||||
rescue Faraday::Error
|
||||
|
||||
handle_response(webhook_event, response:, attempt:)
|
||||
rescue Faraday::SSLError, Faraday::TimeoutError, Faraday::ConnectionFailed => e
|
||||
handle_error(webhook_event, attempt:, error_message: e.class.name.split('::').last)
|
||||
rescue Faraday::Error => e
|
||||
handle_error(webhook_event, attempt:, error_message: e.message&.truncate(100))
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def create_webhook_event(webhook_url, event_uuid:, event_type:, record:)
|
||||
return if event_uuid.blank?
|
||||
|
||||
WebhookEvent.create_with(
|
||||
event_type:,
|
||||
record:,
|
||||
account_id: webhook_url.account_id,
|
||||
status: 'pending'
|
||||
).find_or_create_by!(webhook_url:, uuid: event_uuid)
|
||||
end
|
||||
|
||||
def handle_response(webhook_event, response:, attempt:)
|
||||
return response unless webhook_event
|
||||
|
||||
is_error = response.status.to_i >= 400
|
||||
|
||||
WebhookAttempt.create!(
|
||||
webhook_event:,
|
||||
response_body: is_error ? response.body&.truncate(100) : nil,
|
||||
response_status_code: response.status,
|
||||
attempt:
|
||||
)
|
||||
|
||||
webhook_event.update!(status: is_error ? 'error' : 'success')
|
||||
|
||||
response
|
||||
rescue StandardError
|
||||
raise if Rails.env.local?
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def handle_error(webhook_event, error_message:, attempt:)
|
||||
return unless webhook_event
|
||||
|
||||
WebhookAttempt.create!(
|
||||
webhook_event:,
|
||||
response_body: error_message,
|
||||
response_status_code: 0,
|
||||
attempt:
|
||||
)
|
||||
|
||||
webhook_event.update!(status: 'error')
|
||||
|
||||
nil
|
||||
rescue StandardError
|
||||
raise if Rails.env.local?
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,7 +36,7 @@ module Submissions
|
||||
|
||||
TEXT_LEFT_MARGIN = 1
|
||||
TEXT_TOP_MARGIN = 1
|
||||
MAX_PAGE_ROTATE = 20
|
||||
MAX_PAGE_ROTATE = 50
|
||||
|
||||
A4_SIZE = [595, 842].freeze
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ module Submitters
|
||||
end
|
||||
|
||||
[sql, number, weight, number.length > 1 ? number.delete_prefix('0') : number, weight]
|
||||
elsif keyword.match?(/[^\p{L}\d&@.\-]/)
|
||||
elsif keyword.match?(/[^\p{L}\d&@.\-]/) || keyword.match?(/[.\-]{2,}/)
|
||||
terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq
|
||||
|
||||
if terms.size > 1
|
||||
|
||||
@@ -16,10 +16,7 @@ module Submitters
|
||||
unless submitter.submission_events.exists?(event_type: 'start_form')
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request)
|
||||
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'form.started').each do |webhook_url|
|
||||
SendFormStartedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(submitter, 'form.started')
|
||||
end
|
||||
|
||||
update_submitter!(submitter, params, request, validate_required:)
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WebhookUrls
|
||||
EVENT_TYPE_TO_JOB_CLASS = {
|
||||
'form.started' => SendFormStartedWebhookRequestJob,
|
||||
'form.completed' => SendFormCompletedWebhookRequestJob,
|
||||
'form.declined' => SendFormDeclinedWebhookRequestJob,
|
||||
'form.viewed' => SendFormViewedWebhookRequestJob,
|
||||
'submission.created' => SendSubmissionCreatedWebhookRequestJob,
|
||||
'submission.completed' => SendSubmissionCompletedWebhookRequestJob,
|
||||
'submission.expired' => SendSubmissionExpiredWebhookRequestJob,
|
||||
'submission.archived' => SendSubmissionArchivedWebhookRequestJob,
|
||||
'template.created' => SendTemplateCreatedWebhookRequestJob,
|
||||
'template.updated' => SendTemplateUpdatedWebhookRequestJob
|
||||
}.freeze
|
||||
|
||||
EVENT_TYPE_ID_KEYS = {
|
||||
'form' => 'submitter_id',
|
||||
'submission' => 'submission_id',
|
||||
'template' => 'template_id'
|
||||
}.freeze
|
||||
|
||||
module_function
|
||||
|
||||
def for_account_id(account_id, events)
|
||||
@@ -24,4 +43,26 @@ module WebhookUrls
|
||||
(account_urls.present? ? WebhookUrl.none : linked_urls)
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_events(records, event_type)
|
||||
args = []
|
||||
|
||||
id_key = EVENT_TYPE_ID_KEYS.fetch(event_type.split('.').first)
|
||||
|
||||
Array.wrap(records).group_by(&:account_id).each do |account_id, account_records|
|
||||
webhook_urls = for_account_id(account_id, event_type)
|
||||
|
||||
account_records.each do |record|
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
webhook_urls.each do |webhook_url|
|
||||
next unless webhook_url.events.include?(event_type)
|
||||
|
||||
args << [{ id_key => record.id, 'webhook_url_id' => webhook_url.id, 'event_uuid' => event_uuid }]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Sidekiq::Client.push_bulk('class' => EVENT_TYPE_TO_JOB_CLASS[event_type], 'args' => args)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,8 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -38,7 +39,8 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -57,7 +59,8 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['form.declined'])
|
||||
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -65,8 +68,11 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -75,6 +81,7 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submitter_id']).to eq(submitter.id)
|
||||
end
|
||||
@@ -83,7 +90,8 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 21)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 21)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -21,7 +21,8 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -38,7 +39,8 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -57,7 +59,8 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['form.completed'])
|
||||
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -65,8 +68,11 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -75,6 +81,7 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submitter_id']).to eq(submitter.id)
|
||||
end
|
||||
@@ -83,7 +90,8 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -21,7 +21,8 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -38,7 +39,8 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -57,7 +59,8 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['form.declined'])
|
||||
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -65,8 +68,11 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -75,6 +81,7 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submitter_id']).to eq(submitter.id)
|
||||
end
|
||||
@@ -83,7 +90,8 @@ RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -21,7 +21,8 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -38,7 +39,8 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -57,7 +59,8 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['form.started'])
|
||||
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -65,8 +68,11 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -75,6 +81,7 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submitter_id']).to eq(submitter.id)
|
||||
end
|
||||
@@ -83,7 +90,8 @@ RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -18,7 +18,8 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -35,7 +36,8 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -54,7 +56,8 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['submission.created'])
|
||||
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -62,8 +65,11 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -72,6 +78,7 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submission_id']).to eq(submission.id)
|
||||
end
|
||||
@@ -81,7 +88,7 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'attempt' => 11)
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -18,7 +18,8 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -35,7 +36,8 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -54,7 +56,8 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['submission.archived'])
|
||||
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -62,8 +65,11 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -72,6 +78,7 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submission_id']).to eq(submission.id)
|
||||
end
|
||||
@@ -81,7 +88,7 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'attempt' => 21)
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 21)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -18,7 +18,8 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -35,7 +36,8 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -54,7 +56,8 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['submission.completed'])
|
||||
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -62,8 +65,11 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -72,6 +78,7 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submission_id']).to eq(submission.id)
|
||||
end
|
||||
@@ -81,7 +88,7 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'attempt' => 11)
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe SendSubmissionExpiredWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
let(:template) { create(:template, account:, author: user) }
|
||||
let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user, expire_at: 1.day.ago) }
|
||||
let(:webhook_url) { create(:webhook_url, account:, events: ['submission.expired']) }
|
||||
|
||||
before do
|
||||
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
'event_type' => 'submission.expired',
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook'
|
||||
}
|
||||
).once
|
||||
end
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
'event_type' => 'submission.expired',
|
||||
'timestamp' => /.*/,
|
||||
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'DocuSeal.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
end
|
||||
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['submission.archived'])
|
||||
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
args = described_class.jobs.last['args'].first
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submission_id']).to eq(submission.id)
|
||||
end
|
||||
|
||||
it "doesn't send again if the max attempts is reached" do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
expect do
|
||||
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 21)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -17,7 +17,8 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -34,7 +35,8 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -53,7 +55,8 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['template.updated'])
|
||||
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -61,8 +64,11 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -71,6 +77,7 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['template_id']).to eq(template.id)
|
||||
end
|
||||
@@ -79,7 +86,8 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
expect do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -17,7 +17,8 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
end
|
||||
|
||||
it 'sends a webhook request' do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -34,7 +35,8 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
|
||||
it 'sends a webhook request with the secret' do
|
||||
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||
body: {
|
||||
@@ -53,7 +55,8 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||
webhook_url.update!(events: ['template.created'])
|
||||
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid)
|
||||
|
||||
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||
end
|
||||
@@ -61,8 +64,11 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
it 'sends again if the response status is 400 or higher' do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
event_uuid = SecureRandom.uuid
|
||||
|
||||
expect do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => event_uuid)
|
||||
end.to change(described_class.jobs, :size).by(1)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
@@ -71,6 +77,7 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
|
||||
expect(args['attempt']).to eq(1)
|
||||
expect(args['last_status']).to eq(401)
|
||||
expect(args['event_uuid']).to eq(event_uuid)
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['template_id']).to eq(template.id)
|
||||
end
|
||||
@@ -79,7 +86,8 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||
|
||||
expect do
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
|
||||
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
|
||||
end.not_to change(described_class.jobs, :size)
|
||||
|
||||
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||
|
||||
@@ -1026,20 +1026,23 @@ RSpec.describe 'Signing Form' do
|
||||
end
|
||||
end
|
||||
|
||||
it 'sends completed email' do
|
||||
template = create(:template, account:, author:, only_field_types: %w[text signature])
|
||||
submission = create(:submission, template:)
|
||||
submitter = create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
context 'when a form is completed' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[text signature]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) { create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) }
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
before do
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
end
|
||||
|
||||
fill_in 'First Name', with: 'Adam'
|
||||
click_on 'next'
|
||||
click_link 'Type'
|
||||
fill_in 'signature_text_input', with: 'Adam'
|
||||
it 'sends completed email' do
|
||||
fill_in 'First Name', with: 'Adam'
|
||||
click_on 'next'
|
||||
draw_canvas
|
||||
|
||||
expect do
|
||||
click_on 'Sign and Complete'
|
||||
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
|
||||
expect do
|
||||
click_on 'Sign and Complete'
|
||||
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -175,9 +175,9 @@ RSpec.describe 'Webhook Settings' do
|
||||
|
||||
expect do
|
||||
click_button 'Test Webhook'
|
||||
end.to change(SendFormCompletedWebhookRequestJob.jobs, :size).by(1)
|
||||
end.to change(SendTestWebhookRequestJob.jobs, :size).by(1)
|
||||
|
||||
args = SendFormCompletedWebhookRequestJob.jobs.last['args'].first
|
||||
args = SendTestWebhookRequestJob.jobs.last['args'].first
|
||||
|
||||
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||
expect(args['submitter_id']).to eq(submitter.id)
|
||||
|
||||
Reference in New Issue
Block a user