Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/api/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def create_submissions(template, params)
def submissions_params
permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:expire_at, :name, :external_account_id,
:expire_at, :name, :external_account_id, :requires_approval,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
Expand Down
22 changes: 22 additions & 0 deletions app/controllers/api/submitters_approve_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Api
class SubmittersApproveController < ApiBaseController
before_action :load_submitter

def approve
submission = @submitter.submission

submission.update!(approved_at: Time.current) unless submission.approved_at?

render json: Submitters::SerializeForApi.call(@submitter), status: :ok
end

private

def load_submitter
@submitter = Submitter.find_by!(slug: params[:slug])
authorize! :read, @submitter
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def request_changes
unless @submitter.changes_requested_at?
ApplicationRecord.transaction do
@submitter.update!(changes_requested_at: Time.current, completed_at: nil)
@submitter.submission.update!(approved_at: nil)

SubmissionEvents.create_with_tracking_data(
@submitter,
Expand Down
9 changes: 5 additions & 4 deletions app/jobs/process_submitter_completion_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ def enqueue_completed_webhooks(submitter, is_all_completed: false)
'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 &&
!submitter.submission.requires_approval?

SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'webhook_url_id' => webhook.id)
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def create_careerplug_webhook

webhook_urls.create!(
url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'),
events: %w[form.viewed form.started form.completed form.declined template.preferences_updated],
events: %w[form.started form.completed submission.completed form.changes_requested template.preferences_updated],
secret: { 'X-CareerPlug-Secret' => ENV.fetch('CAREERPLUG_WEBHOOK_SECRET') }
)
end
Expand Down
3 changes: 3 additions & 0 deletions app/models/submission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
# Table name: submissions
#
# id :bigint not null, primary key
# approved_at :datetime
# archived_at :datetime
# expire_at :datetime
# name :text
# preferences :text not null
# requires_approval :boolean default(FALSE), not null
# slug :string not null
# source :string not null
# submitters_order :string not null
Expand All @@ -26,6 +28,7 @@
# index_submissions_on_account_id_and_id (account_id,id)
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
# index_submissions_on_approved_at (approved_at)
# index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id)
Expand Down
7 changes: 4 additions & 3 deletions app/models/submitter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ def anonymize_email_events
end

def export_submission_on_status_change
status_fields = %w[completed_at declined_at opened_at sent_at]
return unless saved_changes.keys.intersect?(status_fields)
# status_fields = %w[completed_at declined_at opened_at sent_at]
# return unless saved_changes.keys.intersect?(status_fields)

ExportSubmissionService.new(submission).call
# CP-12761: Disabled - migrating to webhooks. Remove when ATS /api/docuseal/submissions endpoint is cleaned up.
# ExportSubmissionService.new(submission).call
rescue StandardError => e
Rails.logger.error("Failed to export submission on status change: #{e.message}")
end
Expand Down
7 changes: 0 additions & 7 deletions app/views/submissions/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,6 @@
</span>
</div>
<% end %>
<% if signed_in? && submitter && submitter.completed_at? && !submitter.declined_at? && !submitter.changes_requested_at? && current_user == @submission.template.author %>
<div class="mt-2 mb-1">
<%= link_to 'Request Changes', request_changes_submitter_path(submitter.slug),
class: 'btn btn-sm btn-warning w-full',
data: { turbo_frame: :modal } %>
</div>
<% end %>
</div>
</div>
<div class="px-1.5 mb-4">
Expand Down
6 changes: 6 additions & 0 deletions config/locales/i18n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ en: &en
view_form_by_html: '<b>Form viewed</b> by %{submitter_name}'
invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}'
complete_form_by_html: '<b>Submission completed</b> by %{submitter_name}'
form_update_by_html: '<b>Form updated</b> by %{submitter_name}'
request_changes_by_html: '<b>Changes requested</b> for %{submitter_name}'
start_verification_by_html: '<b>Identity verification started</b> by %{submitter_name}'
complete_verification_by_html: '<b>Identity verification completed</b> by %{submitter_name} with %{provider}'
Expand Down Expand Up @@ -1626,6 +1627,7 @@ es: &es
view_form_by_html: '<b>Formulario visto</b> por %{submitter_name}'
invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}'
complete_form_by_html: '<b>Envío completado</b> por %{submitter_name}'
form_update_by_html: '<b>Formulario actualizado</b> por %{submitter_name}'
request_changes_by_html: '<b>Cambios solicitados</b> para %{submitter_name}'
api_complete_form_by_html: '<b>Envío completado vía API</b> por %{submitter_name}'
start_verification_by_html: '<b>Verificación de identidad iniciada</b> por %{submitter_name}'
Expand Down Expand Up @@ -2461,6 +2463,7 @@ it: &it
view_form_by_html: '<b>Modulo visualizzato</b> da %{submitter_name}'
invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}'
complete_form_by_html: '<b>Invio completato</b> da %{submitter_name}'
form_update_by_html: '<b>Modulo aggiornato</b> da %{submitter_name}'
api_complete_form_by_html: '<b>Invio completato tramite API</b> da %{submitter_name}'
start_verification_by_html: "<b>Verifica dell'identità iniziata</b> da %{submitter_name}"
complete_verification_by_html: "<b>Verifica dell'identità completata</b> da %{submitter_name} con %{provider}"
Expand Down Expand Up @@ -3298,6 +3301,7 @@ fr: &fr
view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}'
invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}'
complete_form_by_html: '<b>Soumission terminée</b> par %{submitter_name}'
form_update_by_html: '<b>Formulaire mis à jour</b> par %{submitter_name}'
start_verification_by_html: "<b>Vérification d'identité commencée</b> par %{submitter_name}"
complete_verification_by_html: "<b>Vérification d'identité terminée</b> par %{submitter_name} avec %{provider}"
api_complete_form_by_html: "<b>Soumission terminée via l'API</b> par %{submitter_name}"
Expand Down Expand Up @@ -4134,6 +4138,7 @@ pt: &pt
view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}'
invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}'
complete_form_by_html: '<b>Submissão concluída</b> por %{submitter_name}'
form_update_by_html: '<b>Formulário atualizado</b> por %{submitter_name}'
start_verification_by_html: '<b>Verificação de identidade iniciada</b> por %{submitter_name}'
complete_verification_by_html: '<b>Verificação de identidade concluída</b> por %{submitter_name} com %{provider}'
api_complete_form_by_html: '<b>Submissão concluída via API</b> por %{submitter_name}'
Expand Down Expand Up @@ -4971,6 +4976,7 @@ de: &de
view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}'
invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}'
complete_form_by_html: '<b>Einreichung abgeschlossen</b> von %{submitter_name}'
form_update_by_html: '<b>Formular aktualisiert</b> von %{submitter_name}'
start_verification_by_html: '<b>Identitätsüberprüfung gestartet</b> von %{submitter_name}'
complete_verification_by_html: '<b>Identitätsüberprüfung abgeschlossen</b> von %{submitter_name} mit %{provider}'
api_complete_form_by_html: '<b>Einreichung über API abgeschlossen</b> von %{submitter_name}'
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
resources :submitters, only: [], param: :slug do
member do
post :request_changes, controller: 'submitters_request_changes'
post :approve, controller: 'submitters_approve'
end
end
resources :submissions, only: %i[index show create destroy] do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class AddRequiresApprovalToSubmissions < ActiveRecord::Migration[7.2]
def change
add_column :submissions, :requires_approval, :boolean, default: false, null: false
add_column :submissions, :approved_at, :datetime
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[optional] Might be good to add an index to fetch the approved_at

add_index :submissions, :approved_at

add_index :submissions, :approved_at
end
end
5 changes: 4 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2026_02_06_171605) do
ActiveRecord::Schema[8.0].define(version: 2026_03_11_000001) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql"
Expand Down Expand Up @@ -327,9 +327,12 @@
t.integer "account_id", null: false
t.datetime "expire_at"
t.text "name"
t.boolean "requires_approval", default: false, null: false
t.datetime "approved_at"
t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)"
t.index ["approved_at"], name: "index_submissions_on_approved_at"
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["slug"], name: "index_submissions_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id"
Expand Down
1 change: 1 addition & 0 deletions lib/submissions/create_from_submitters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def call(template:, user:, submissions_attrs:, source:, submitters_order:, param
preferences: set_submission_preferences,
name: with_template ? attrs[:name] : (attrs[:name] || template.name),
expire_at:,
requires_approval: params[:requires_approval] || false,
template_submitters: [], submitters_order:)

template_submitters = template.submitters.deep_dup
Expand Down
1 change: 1 addition & 0 deletions lib/submissions/serialize_for_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def call(submission, submitters = nil, params = {}, with_events: true, with_docu

json = submission.as_json(SERIALIZE_PARAMS)

json['submission_id'] = submission.id
json['external_account_id'] = submission.account&.external_account_id
json['created_by_user'] ||= nil

Expand Down
2 changes: 1 addition & 1 deletion lib/submitters/serialize_for_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def call(submitter, with_template: false, with_events: false, with_documents: tr
def serialize_events(events)
events.map do |event|
event.as_json(only: %i[id submitter_id event_type event_timestamp])
.merge('data' => event.data.slice('reason', 'firstname', 'lastname', 'method', 'country'))
.merge('data' => event.data.slice('reason', 'firstname', 'lastname', 'method', 'country', 'changes'))
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/submitters/serialize_for_webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def call(submitter)
methods: %i[folder_name]
),
'submission' => {
**submitter.submission.slice(:id, :audit_log_url, :combined_document_url, :created_at),
**submitter.submission.slice(:id, :audit_log_url, :combined_document_url, :created_at,
:requires_approval),
status: build_submission_status(submitter.submission),
url: r.submissions_preview_url(submitter.submission.slug, **Docuseal.default_url_options)
})
Expand Down
23 changes: 22 additions & 1 deletion lib/tasks/webhooks.rake
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ namespace :webhooks do
url = 'http://localhost:3000/api/docuseal/events'
secret = { 'X-CareerPlug-Secret' => 'development_webhook_secret' }
sha1 = Digest::SHA1.hexdigest(url)
account_events = %w[form.viewed form.started form.completed form.declined template.preferences_updated]
account_events = %w[form.started form.completed submission.completed
form.changes_requested template.preferences_updated]
partnership_events = %w[template.preferences_updated]

created = 0
Expand Down Expand Up @@ -71,4 +72,24 @@ namespace :webhooks do

puts "Done: #{created} created, #{updated} updated"
end

desc 'Backfill account webhook URLs to include required ATS events'
task backfill_account_events: :environment do
required_events = %w[form.started form.completed submission.completed
form.changes_requested template.preferences_updated]

updated = 0

WebhookUrl.where(partnership_id: nil).find_each do |webhook_url|
missing = required_events - webhook_url.events

next if missing.empty?

webhook_url.update!(events: (webhook_url.events + missing).uniq)
updated += 1
puts "Updated account_id=#{webhook_url.account_id} id=#{webhook_url.id}: added #{missing.join(', ')}"
end

puts "Done: #{updated} webhook URL(s) updated"
end
end
16 changes: 16 additions & 0 deletions spec/jobs/process_submitter_completion_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@
end.to raise_error(ActiveRecord::RecordNotFound)
end

context 'when submission requires approval' do
let(:webhook) { create(:webhook_url, account:, events: ['submission.completed']) }
let(:submission) { create(:submission, template:, created_by_user: user, requires_approval: true) }

before do
webhook
submission.submitters.update_all(completed_at: Time.current)
end

it 'does not enqueue submission.completed webhook' do
expect do
described_class.new.perform('submitter_id' => submitter.id)
end.not_to change(SendSubmissionCompletedWebhookRequestJob.jobs, :size)
end
end

context 'when all submitters are completed' do
let(:submitter2) { create(:submitter, submission:, uuid: SecureRandom.uuid, completed_at: Time.current) }

Expand Down
27 changes: 27 additions & 0 deletions spec/lib/submissions/create_from_submitters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,33 @@ def call(template:, submitters_order:)
end
end

describe 'requires_approval' do
it 'persists requires_approval from params onto the submission' do
submissions = described_class.call(
template:,
user:,
submissions_attrs: [{ 'submitters' => submitter_attrs }.with_indifferent_access],
source: :api,
submitters_order: 'simultaneous',
params: { requires_approval: true }
)

expect(submissions.first.requires_approval).to be(true)
end

it 'defaults requires_approval to false when not provided' do
submissions = described_class.call(
template:,
user:,
submissions_attrs: [{ 'submitters' => submitter_attrs }.with_indifferent_access],
source: :api,
submitters_order: 'simultaneous'
)

expect(submissions.first.requires_approval).to be(false)
end
end

describe 'single_sided skipping' do
before do
manager_uuid = template.submitters[1]['uuid']
Expand Down
4 changes: 2 additions & 2 deletions spec/models/account_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@

expect(webhook).to be_present
expect(webhook.events).to match_array(%w[
form.viewed
form.started
form.completed
form.declined
submission.completed
form.changes_requested
template.preferences_updated
])
end
Expand Down
38 changes: 38 additions & 0 deletions spec/models/submission_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: submissions
#
# id :bigint not null, primary key
# approved_at :datetime
# archived_at :datetime
# expire_at :datetime
# name :text
# preferences :text not null
# requires_approval :boolean default(FALSE), not null
# slug :string not null
# source :string not null
# submitters_order :string not null
# template_fields :text
# template_schema :text
# template_submitters :text
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
# created_by_user_id :integer
# template_id :integer
#
# Indexes
#
# index_submissions_on_account_id_and_id (account_id,id)
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
# index_submissions_on_approved_at (approved_at)
# index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id)
#
# Foreign Keys
#
# fk_rails_... (created_by_user_id => users.id)
# fk_rails_... (template_id => templates.id)
#
RSpec.describe Submission do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
Expand Down
Loading
Loading