Merge pull request #8 from CareerPlug/CP-9698-export-docuseal-submission-status

CP-9698 export docuseal submission status
pull/544/head
Ryan Arakawa 4 months ago committed by GitHub
commit 233ab7c255
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -6,6 +6,7 @@ class SubmitFormController < ApplicationController
around_action :with_browser_locale, only: %i[show completed success]
skip_before_action :authenticate_user!
skip_authorization_check
skip_before_action :verify_authenticity_token, only: :update
before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_locked_page, only: :show

@ -67,6 +67,7 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) }
after_update :export_submission_on_status_change
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status
@ -122,4 +123,13 @@ class Submitter < ApplicationRecord
event.update!(email: Digest::MD5.base64digest(event.email))
end
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)
ExportSubmissionService.new(submission).call
rescue StandardError => e
Rails.logger.error("Failed to export submission on status change: #{e.message}")
end
end

@ -9,7 +9,7 @@ class ExportService
@error_message = nil
end
def set_error(message)
def record_error(message)
@error_message = message
end
@ -40,8 +40,4 @@ class ExportService
def export_location
@export_location ||= ExportLocation.default_location
end
def set_error(message)
@error_message = message
end
end

@ -9,8 +9,10 @@ class ExportSubmissionService < ExportService
end
def call
export_location = ExportLocation.default_location
if export_location&.submissions_endpoint.blank?
set_error('Export failed: Submission export endpoint is not configured.')
record_error('Export failed: Submission export endpoint is not configured.')
return false
end
@ -20,18 +22,18 @@ class ExportSubmissionService < ExportService
if response&.success?
true
else
set_error("Failed to export submission ##{submission.id} events.")
record_error("Failed to export submission ##{submission.id} events.")
false
end
rescue Faraday::Error => e
Rails.logger.error("Failed to export submission Faraday: #{e.message}")
Rollbar.error("Failed to export submission: #{e.message}") if defined?(Rollbar)
set_error("Network error occurred during export: #{e.message}")
record_error("Network error occurred during export: #{e.message}")
false
rescue StandardError => e
Rails.logger.error("Failed to export submission: #{e.message}")
Rollbar.error(e) if defined?(Rollbar)
set_error("An unexpected error occurred during export: #{e.message}")
record_error("An unexpected error occurred during export: #{e.message}")
false
end
@ -39,9 +41,38 @@ class ExportSubmissionService < ExportService
def build_payload
{
submission_id: submission.id,
external_submission_id: submission.id,
template_name: submission.template&.name,
events: submission.submission_events.order(updated_at: :desc).limit(1)
status: submission_status,
submitter_data: submission.submitters.map do |submitter|
{
external_submitter_id: submitter.slug,
name: submitter.name,
email: submitter.email,
status: submitter.status,
completed_at: submitter.completed_at,
declined_at: submitter.declined_at
}
end,
created_at: submission.created_at,
updated_at: submission.updated_at
}
end
def submission_status
# The status is tracked for each submitter, so we need to check the status of all submitters
statuses = submission.submitters.map(&:status)
if statuses.include?('declined')
'declined'
elsif statuses.all?('completed')
'completed'
elsif statuses.any?('opened')
'in_progress'
elsif statuses.any?('sent')
'sent'
else
'pending'
end
end
end

@ -15,18 +15,18 @@ class ExportTemplateService < ExportService
else
Rails.logger.error("Failed to export template to third party: #{response&.status}")
Rollbar.error("#{export_location.name} template export API error: #{response&.status}") if defined?(Rollbar)
set_error('Failed to export template to third party')
record_error('Failed to export template to third party')
false
end
rescue Faraday::Error => e
Rails.logger.error("Failed to export template Faraday: #{e.message}")
Rollbar.error("Failed to export template: #{e.message}") if defined?(Rollbar)
set_error("Network error occurred during template export: #{e.message}")
record_error("Network error occurred during template export: #{e.message}")
false
rescue StandardError => e
Rails.logger.error("Failed to export template: #{e.message}")
Rollbar.error(e) if defined?(Rollbar)
set_error("An unexpected error occurred during template export: #{e.message}")
record_error("An unexpected error occurred during template export: #{e.message}")
false
end
end

@ -0,0 +1,165 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Submitter do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:template) { create(:template, account: account, author: user) }
let(:submission) do
create(:submission, :with_submitters, template: template, account: account, created_by_user: user)
end
let(:submitter) { submission.submitters.first }
describe '#status' do
context 'when submitter is awaiting' do
it 'returns awaiting' do
expect(submitter.status).to eq('awaiting')
end
end
context 'when submitter is sent' do
before { submitter.update!(sent_at: Time.current) }
it 'returns sent' do
expect(submitter.status).to eq('sent')
end
end
context 'when submitter is opened' do
before do
submitter.update!(sent_at: Time.current, opened_at: Time.current)
end
it 'returns opened' do
expect(submitter.status).to eq('opened')
end
end
context 'when submitter is completed' do
before do
submitter.update!(
sent_at: Time.current,
opened_at: Time.current,
completed_at: Time.current
)
end
it 'returns completed' do
expect(submitter.status).to eq('completed')
end
end
context 'when submitter is declined' do
before { submitter.update!(declined_at: Time.current) }
it 'returns declined' do
expect(submitter.status).to eq('declined')
end
end
context 'when submitter is declined but also completed' do
before do
submitter.update!(
completed_at: Time.current,
declined_at: Time.current
)
end
it 'returns declined (declined takes precedence)' do
expect(submitter.status).to eq('declined')
end
end
end
describe '#export_submission_on_status_change' do
let(:export_location) { create(:export_location, :with_submissions_endpoint) }
let(:export_service) { instance_double(ExportSubmissionService) }
before do
allow(ExportLocation).to receive(:default_location).and_return(export_location)
allow(ExportSubmissionService).to receive(:new).with(submission).and_return(export_service)
allow(export_service).to receive(:call).and_return(true)
end
context 'when status-related field changes' do
it 'calls ExportSubmissionService when completed_at changes' do
submitter.update!(completed_at: Time.current)
expect(ExportSubmissionService).to have_received(:new).with(submission)
expect(export_service).to have_received(:call)
end
it 'calls ExportSubmissionService when declined_at changes' do
submitter.update!(declined_at: Time.current)
expect(ExportSubmissionService).to have_received(:new).with(submission)
expect(export_service).to have_received(:call)
end
it 'calls ExportSubmissionService when opened_at changes' do
submitter.update!(opened_at: Time.current)
expect(ExportSubmissionService).to have_received(:new).with(submission)
expect(export_service).to have_received(:call)
end
it 'calls ExportSubmissionService when sent_at changes' do
submitter.update!(sent_at: Time.current)
expect(ExportSubmissionService).to have_received(:new).with(submission)
expect(export_service).to have_received(:call)
end
end
context 'when non-status field changes' do
it 'does not call ExportSubmissionService when email changes' do
submitter.update!(email: 'new@example.com')
expect(ExportSubmissionService).not_to have_received(:new)
expect(export_service).not_to have_received(:call)
end
it 'does not call ExportSubmissionService when name changes' do
submitter.update!(name: 'New Name')
expect(ExportSubmissionService).not_to have_received(:new)
expect(export_service).not_to have_received(:call)
end
end
context 'when export service raises an error' do
before do
allow(export_service).to receive(:call).and_raise(StandardError.new('Export failed'))
allow(Rails.logger).to receive(:error)
end
it 'logs the error and does not re-raise' do
expect { submitter.update!(completed_at: Time.current) }.not_to raise_error
expect(Rails.logger).to have_received(:error).with(
'Failed to export submission on status change: Export failed'
)
end
end
context 'when ExportLocation.default_location returns nil' do
before do
allow(ExportLocation).to receive(:default_location).and_return(nil)
allow(export_service).to receive(:call).and_return(false)
end
it 'calls ExportSubmissionService but service handles nil export location' do
submitter.update!(completed_at: Time.current)
expect(ExportSubmissionService).to have_received(:new).with(submission)
expect(export_service).to have_received(:call)
end
end
context 'when export location has no submissions_endpoint' do
before do
allow(export_location).to receive(:submissions_endpoint).and_return(nil)
allow(export_service).to receive(:call).and_return(false)
end
it 'calls ExportSubmissionService but service handles missing endpoint' do
submitter.update!(completed_at: Time.current)
expect(ExportSubmissionService).to have_received(:new).with(submission)
expect(export_service).to have_received(:call)
end
end
end
end

@ -6,7 +6,9 @@ RSpec.describe ExportSubmissionService do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:template) { create(:template, account: account, author: user) }
let(:submission) { create(:submission, template: template, account: account) }
let(:submission) do
create(:submission, :with_submitters, template: template, account: account, created_by_user: user)
end
let(:export_location) { create(:export_location, :with_submissions_endpoint) }
let(:service) { described_class.new(submission) }
let(:faraday_connection) { instance_double(Faraday::Connection) }
@ -143,28 +145,48 @@ RSpec.describe ExportSubmissionService do
before do
allow(request_double).to receive(:body=)
allow(faraday_connection).to receive(:post).and_yield(request_double).and_return(faraday_response)
allow(faraday_connection).to receive(:post)
.with(export_location.submissions_endpoint)
.and_yield(request_double)
.and_return(faraday_response)
allow(faraday_response).to receive(:success?).and_return(true)
end
it 'includes submission_id in payload' do
allow(request_double).to receive(:body=) do |body|
expect(JSON.parse(body)).to include('submission_id' => submission.id)
end
service.call
allow(Submitter).to receive(:after_update)
submission.submitters.first.update!(name: 'John Doe', email: 'john@example.com', completed_at: Time.current)
submission.submitters << create(
:submitter,
submission: submission,
account: account,
name: 'Jane Smith',
email: 'jane@example.com',
opened_at: Time.current,
uuid: SecureRandom.uuid
)
end
it 'includes template_name in payload' do
allow(request_double).to receive(:body=) do |body|
expect(JSON.parse(body)).to include('template_name' => submission.template.name)
end
service.call
end
it 'includes recent events in payload' do
it 'builds correct payload structure with all required fields' do
allow(request_double).to receive(:body=) do |body|
parsed_body = JSON.parse(body)
expect(parsed_body).to have_key('events')
expect(parsed_body).to include(
'external_submission_id' => submission.id,
'template_name' => submission.template.name,
'status' => 'in_progress'
)
expect(parsed_body).to have_key('created_at')
expect(parsed_body).to have_key('updated_at')
expect(parsed_body['submitter_data']).to be_an(Array)
expect(parsed_body['submitter_data'].length).to eq(2)
completed_submitter = parsed_body['submitter_data'].find { |s| s['status'] == 'completed' }
expect(completed_submitter).to include(
'name' => 'John Doe',
'email' => 'john@example.com',
'status' => 'completed'
)
expect(completed_submitter).to have_key('external_submitter_id')
end
service.call
end
@ -189,7 +211,10 @@ RSpec.describe ExportSubmissionService do
before do
allow(export_location).to receive(:extra_params).and_return({ 'api_key' => 'test_key', 'version' => '1.0' })
allow(request_double).to receive(:body=)
allow(faraday_connection).to receive(:post).and_yield(request_double).and_return(faraday_response)
allow(faraday_connection).to receive(:post)
.with(export_location.submissions_endpoint)
.and_yield(request_double)
.and_return(faraday_response)
allow(faraday_response).to receive(:success?).and_return(true)
end

Loading…
Cancel
Save