add webhook events
This commit is contained in:
committed by
Pete Matsyburka
parent
1b60b42428
commit
988a5361a6
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookEventsController < ApplicationController
|
||||
load_and_authorize_resource :webhook_url, parent: false, only: %i[show resend], id_param: :webhook_id
|
||||
|
||||
def show
|
||||
@webhook_event = @webhook_url.webhook_events.find_by!(uuid: params[:id])
|
||||
@webhook_attempts = @webhook_event.webhook_attempts.order(created_at: :desc)
|
||||
|
||||
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
|
||||
@webhook_event = @webhook_url.webhook_events.find_by!(uuid: params[:id])
|
||||
|
||||
id_key = WebhookUrls::EVENT_TYPE_ID_KEYS.fetch(@webhook_event.event_type.split('.').first)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
head :ok
|
||||
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
|
||||
|
||||
@@ -42,8 +58,11 @@ class WebhookSettingsController < ApplicationController
|
||||
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
|
||||
|
||||
@@ -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?
|
||||
response_status_code.to_i / 100 == 2
|
||||
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: -> { {} }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<%= render 'shared/turbo_drawer', title: @webhook_event.event_type, close_after_submit: false do %>
|
||||
<div class="relative px-4 py-4">
|
||||
<ol class="relative border-s border-base-300 space-y-6 ml-3">
|
||||
<% if @webhook_event.status == 'error' %>
|
||||
<% last_attempt = @webhook_attempts.select { |e| SendWebhookRequest::AUTOMATED_RETRY_RANGE.cover?(e.attempt) }.max_by(&:attempt) %>
|
||||
<% if SendWebhookRequest::AUTOMATED_RETRY_RANGE.cover?(last_attempt&.attempt) %>
|
||||
<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' %>
|
||||
<%= button_to button_title(title: t('resend'), disabled_with: 'sending', 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), class: 'absolute right-4 top-3 btn btn-neutral btn-sm text-white', method: :post %>
|
||||
<% end %>
|
||||
<% 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,75 @@
|
||||
<% 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">
|
||||
<% @webhook_events.each do |event| %>
|
||||
<div class="group relative hover:cursor-pointer hover:bg-base-200">
|
||||
<a href="<%= settings_webhook_event_path(@webhook_url.id, 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 event.status == 'success' %>
|
||||
<div class="btn btn-outline btn-xs btn-success bg-lime-50 gap-2">
|
||||
<%= svg_icon('check', class: 'w-4 h-4 shrink-0 stroke-2') %>
|
||||
</div>
|
||||
<% elsif event.status == 'pending' %>
|
||||
<div class="btn btn-outline btn-xs btn-info bg-blue-50 gap-2">
|
||||
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0 stroke-2') %>
|
||||
</div>
|
||||
<% elsif event.status == 'error' %>
|
||||
<div class="btn btn-outline btn-xs btn-error bg-red-50 gap-2">
|
||||
<%= svg_icon('x', class: 'w-4 h-4 shrink-0') %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div><%= event.event_type %></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<%= button_to button_title(title: t('resend'), disabled_with: t('sending'), 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, event.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 %>
|
||||
<span><%= l(event.created_at, locale: current_account.locale, format: :short) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</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 %>
|
||||
<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 +165,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>
|
||||
|
||||
Reference in New Issue
Block a user