export submission status

* when submitter status changes, export to external API
* update ExportSubmissionService to send payload to external API
pull/544/head
Ryan Arakawa 4 months ago
parent c7c0cb17f7
commit 5fe00d3206

@ -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

@ -68,6 +68,7 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) }
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
after_update :export_submission_on_status_change
def status
if declined_at?
@ -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 & status_fields).any?
ExportSubmissionService.new(submission).call
rescue => e
Rails.logger.error("Failed to export submission on status change: #{e.message}")
end
end

@ -9,6 +9,8 @@ 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.')
return false
@ -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? { |s| s == 'completed' }
'completed'
elsif statuses.any? { |s| s == 'opened' }
'in_progress'
elsif statuses.any? { |s| s == 'sent' }
'sent'
else
'pending'
end
end
end

@ -0,0 +1,161 @@
# 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) { create(:submission, :with_submitters, template: template, account: account, created_by_user: user) }
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

@ -140,16 +140,19 @@ RSpec.describe ExportSubmissionService do
describe 'payload building' do
let(:request_double) { instance_double(Faraday::Request, body: nil) }
let(:submitter1) { create(:submitter, submission: submission, account: account, name: 'John Doe', email: 'john@example.com', completed_at: Time.current, uuid: SecureRandom.uuid) }
let(:submitter2) { create(:submitter, submission: submission, account: account, name: 'Jane Smith', email: 'jane@example.com', opened_at: Time.current, uuid: SecureRandom.uuid) }
before do
submission.submitters << [submitter1, submitter2]
allow(request_double).to receive(:body=)
allow(faraday_connection).to receive(:post).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
it 'includes external_submission_id in payload' do
allow(request_double).to receive(:body=) do |body|
expect(JSON.parse(body)).to include('submission_id' => submission.id)
expect(JSON.parse(body)).to include('external_submission_id' => submission.id)
end
service.call
end
@ -161,10 +164,52 @@ RSpec.describe ExportSubmissionService do
service.call
end
it 'includes recent events in payload' do
it 'includes submission status in payload' do
allow(request_double).to receive(:body=) do |body|
expect(JSON.parse(body)).to include('status' => 'in_progress')
end
service.call
end
it 'includes submitter_data array in payload' do
allow(request_double).to receive(:body=) do |body|
parsed_body = JSON.parse(body)
expect(parsed_body).to have_key('submitter_data')
expect(parsed_body['submitter_data']).to be_an(Array)
expect(parsed_body['submitter_data'].length).to eq(2)
end
service.call
end
it 'includes correct submitter data in payload' do
allow(request_double).to receive(:body=) do |body|
parsed_body = JSON.parse(body)
expect(parsed_body).to have_key('events')
submitter_data = parsed_body['submitter_data']
first_submitter = submitter_data.find { |s| s['email'] == 'john@example.com' }
expect(first_submitter).to include(
'external_submitter_id' => submitter1.slug,
'name' => 'John Doe',
'email' => 'john@example.com',
'status' => 'completed'
)
second_submitter = submitter_data.find { |s| s['email'] == 'jane@example.com' }
expect(second_submitter).to include(
'external_submitter_id' => submitter2.slug,
'name' => 'Jane Smith',
'email' => 'jane@example.com',
'status' => 'opened'
)
end
service.call
end
it 'includes created_at and updated_at in payload' do
allow(request_double).to receive(:body=) do |body|
parsed_body = JSON.parse(body)
expect(parsed_body).to have_key('created_at')
expect(parsed_body).to have_key('updated_at')
end
service.call
end
@ -183,6 +228,59 @@ RSpec.describe ExportSubmissionService do
end
end
describe '#submission_status' do
let(:service) { described_class.new(submission) }
context 'with multiple submitters' do
let(:submitter1) { create(:submitter, submission: submission, account: account, uuid: SecureRandom.uuid) }
let(:submitter2) { create(:submitter, submission: submission, account: account, uuid: SecureRandom.uuid) }
before do
submission.submitters << [submitter1, submitter2]
end
it 'returns declined when any submitter is declined' do
submitter1.declined_at = Time.current
submitter2.completed_at = Time.current
expect(service.send(:submission_status)).to eq('declined')
end
it 'returns completed when all submitters are completed' do
submitter1.completed_at = Time.current
submitter2.completed_at = Time.current
expect(service.send(:submission_status)).to eq('completed')
end
it 'returns in_progress when any submitter is opened but not all completed' do
submitter1.opened_at = Time.current
submitter2.sent_at = Time.current
expect(service.send(:submission_status)).to eq('in_progress')
end
it 'returns sent when any submitter is sent but none opened' do
submitter1.sent_at = Time.current
expect(service.send(:submission_status)).to eq('sent')
end
it 'returns pending when no submitters have been sent' do
expect(service.send(:submission_status)).to eq('pending')
end
end
context 'with single submitter' do
let(:submitter) { create(:submitter, submission: submission, account: account, uuid: SecureRandom.uuid) }
before do
submission.submitters << submitter
end
it 'returns the submitter status when single submitter' do
submitter.opened_at = Time.current
expect(service.send(:submission_status)).to eq('in_progress')
end
end
end
describe 'extra_params handling' do
let(:request_double) { instance_double(Faraday::Request, body: nil) }

Loading…
Cancel
Save