Skip to content
Closed
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 @@ -167,7 +167,7 @@ def create_submissions(template, params)
template:,
user: current_user,
source: :api,
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
submitters_order: params[:submitters_order] || params[:order] || template.effective_submitters_order,
submissions_attrs:,
params:
)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def create
Submissions.create_from_submitters(template: @template,
user: current_user,
source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
submitters_order: @template.effective_submitters_order,
submissions_attrs: submissions_params[:submission].to_h.values,
params: params.merge('send_completed_email' => true))
end
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/submit_form_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ def show
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)

return render :awaiting if (@form_configs[:enforce_signing_order] ||
submission.template&.preferences&.dig('submitters_order') == 'preserved') &&
submission.template_signing_order.in?(
%w[employee_then_manager manager_then_employee]
)) &&
!Submitters.current_submitter_order?(@submitter)

Submissions.preload_with_pages(submission)
Expand Down
19 changes: 17 additions & 2 deletions app/controllers/templates_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ def create
end

def update
@template.assign_attributes(template_params)
# Capture current submitters_order before any changes
old_submitters_order = @template.preferences['submitters_order']

@template.assign_attributes(template_params)
is_name_changed = @template.name_changed?

@template.save!
Expand All @@ -109,7 +111,13 @@ def update

enqueue_template_updated_webhooks(@template)

head :ok
# If submitters_order changed (e.g., fields removed making it single_sided), fire preferences webhook
new_submitters_order = @template.preferences['submitters_order']
if old_submitters_order != new_submitters_order && new_submitters_order.present?
enqueue_template_preferences_updated_webhooks(@template)
end

render json: { preferences: @template.preferences }
end

def destroy
Expand Down Expand Up @@ -173,6 +181,13 @@ def enqueue_template_updated_webhooks(template)
end
end

def enqueue_template_preferences_updated_webhooks(template)
WebhookUrls.for_template(template, 'template.preferences_updated').each do |webhook_url|
SendTemplatePreferencesUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end

def handle_account_override
return unless authorized_clone_account_id?(params[:account_id])

Expand Down
29 changes: 28 additions & 1 deletion app/controllers/templates_preferences_controller.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
# frozen_string_literal: true

class TemplatesPreferencesController < ApplicationController
include IframeAuthentication
include PartnershipContext

skip_before_action :verify_authenticity_token
skip_before_action :authenticate_via_token!

before_action :authenticate_from_referer
load_and_authorize_resource :template

def show; end

def create
authorize!(:update, @template)

old_submitters_order = @template.preferences['submitters_order']
@template.preferences = @template.preferences.merge(template_params[:preferences])
@template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? }
@templahttp://app.lvh.me:3000/retain/team/tasks_list_builder/13/editte.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? }

# Handle single_sided case (when template has < 2 unique submitters)
if @template.unique_submitter_uuids.size < 2 && @template.preferences['submitters_order'].present?
@template.preferences['submitters_order'] = 'single_sided'
end

@template.save!

# Enqueue webhook if submitters_order changed
new_submitters_order = @template.preferences['submitters_order']
if old_submitters_order != new_submitters_order && new_submitters_order.present?
enqueue_template_preferences_updated_webhooks(@template)
end

head :ok
end

Expand Down Expand Up @@ -51,4 +71,11 @@ def template_params
end
end
end

def enqueue_template_preferences_updated_webhooks(template)
WebhookUrls.for_template(template, 'template.preferences_updated').each do |webhook_url|
SendTemplatePreferencesUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
end
16 changes: 13 additions & 3 deletions app/models/submission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Submission < ApplicationRecord
serialize :preferences, coder: JSON

attribute :source, :string, default: 'link'
attribute :submitters_order, :string, default: 'random'
attribute :submitters_order, :string, default: 'employee_then_manager'

attribute :slug, :string, default: -> { SecureRandom.base58(14) }

Expand Down Expand Up @@ -94,10 +94,16 @@ class Submission < ApplicationRecord
}, scope: false, prefix: true

enum :submitters_order, {
random: 'random',
preserved: 'preserved'
single_sided: 'single_sided',
employee_then_manager: 'employee_then_manager',
manager_then_employee: 'manager_then_employee',
simultaneous: 'simultaneous'
}, scope: false, prefix: true

def signing_order_enforced?
template_signing_order.in?(%w[employee_then_manager manager_then_employee])
end

def expired?
expire_at && expire_at <= Time.current
end
Expand All @@ -106,6 +112,10 @@ def last_completed_submitter
submitters.where.not(completed_at: nil).order(:completed_at).last
end

def template_signing_order
template&.preferences&.dig('submitters_order')
end

def schema_documents
if template_id?
template_schema_documents
Expand Down
29 changes: 29 additions & 0 deletions app/models/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class Template < ApplicationRecord
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy

before_validation :maybe_set_default_folder, on: :create
before_save :update_submitters_order, if: :fields_changed?

attribute :preferences, :string, default: -> { {} }
attribute :fields, :string, default: -> { [] }
Expand Down Expand Up @@ -87,6 +88,15 @@ def application_key
external_id
end

def unique_submitter_uuids
fields.filter_map { |f| f['submitter_uuid'] }.uniq
end

def effective_submitters_order
preferences['submitters_order'].presence ||
(unique_submitter_uuids.size < 2 ? 'single_sided' : 'employee_then_manager')
end

private

def maybe_set_default_folder
Expand All @@ -96,4 +106,23 @@ def maybe_set_default_folder
self.folder ||= partnership.default_template_folder(author)
end
end

def update_submitters_order
submitter_count = unique_submitter_uuids.size
current_order = preferences['submitters_order']

if submitter_count < 2
# Always set to single_sided for templates with 0 or 1 submitter
preferences['submitters_order'] = 'single_sided'
elsif submitter_count == 2
# Set to employee_then_manager when there are exactly 2 submitters
# Only set if not already configured to something else
if current_order.blank? || current_order == 'single_sided'
preferences['submitters_order'] = 'employee_then_manager'
end
elsif current_order == 'single_sided'
# Clear single_sided if template now has 3+ submitters
preferences.delete('submitters_order')
end
end
end
9 changes: 7 additions & 2 deletions lib/params/submission_create_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Params
class SubmissionCreateValidator < BaseValidator
def call
binding.pry
if params[:submission].blank? && (params[:emails].present? || params[:email].present?)
validate_creation_from_emails(params)
elsif params.key?(:submitters)
Expand Down Expand Up @@ -56,7 +57,9 @@ def validate_creation_from_submitters(params)
required(message_params, :body)
end

value_in(params, :order, %w[preserved random], allow_nil: true)
value_in(
params, :order, %w[employee_then_manager manager_then_employee simultaneous single_sided], allow_nil: true
)

if params[:submitters].present?
in_path(params, :submitters) do |submitters_params|
Expand Down Expand Up @@ -117,7 +120,9 @@ def validate_creation_from_submission(params)
required(message_params, :body)
end

value_in(params, :order, %w[preserved random], allow_nil: true)
value_in(
params, :order, %w[employee_then_manager manager_then_employee simultaneous single_sided], allow_nil: true
)

return true if params[:submission].is_a?(Array)

Expand Down
17 changes: 13 additions & 4 deletions lib/submissions.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Submissions
DEFAULT_SUBMITTERS_ORDER = 'random'
DEFAULT_SUBMITTERS_ORDER = 'single_sided'

PRELOAD_ALL_PAGES_AMOUNT = 200

Expand Down Expand Up @@ -143,9 +143,18 @@ def send_signature_requests(submissions, delay: nil)

submitters = submission.submitters.reject(&:completed_at?)

if submission.submitters_order_preserved?
first_submitter =
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first
if submission.signing_order_enforced?
first_submitter = if submission.template_signing_order == 'manager_then_employee'
# For manager_then_employee, send to the second submitter first
submission.template_submitters[1..].filter_map do |s|
submitters.find { |e| e.uuid == s['uuid'] }
end.first
else
# For employee_then_manager and preserved, send to the first submitter
submission.template_submitters.filter_map do |s|
submitters.find { |e| e.uuid == s['uuid'] }
end.first
end

Submitters.send_signature_requests([first_submitter], delay_seconds:) if first_submitter
else
Expand Down
23 changes: 22 additions & 1 deletion lib/submissions/create_from_submitters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ def call(template:, user:, submissions_attrs:, source:, submitters_order:, param
submission.template_schema = submission.template.schema if submission.template_schema.blank?

uuid = template_submitter['uuid']

# Skip submitters without fields for single_sided forms
next if submitters_order == 'single_sided' && template.unique_submitter_uuids.exclude?(uuid)
else
if submitter_attrs[:roles].present? && submitter_attrs[:roles].size == 1
submitter_attrs[:role] = submitter_attrs[:roles].first
Expand All @@ -46,6 +49,9 @@ def call(template:, user:, submissions_attrs:, source:, submitters_order:, param
next if uuid.blank?
next if submitter_attrs.slice('email', 'phone', 'name').compact_blank.blank?

# Skip submitters without fields for single_sided forms
next if submitters_order == 'single_sided' && template.unique_submitter_uuids.exclude?(uuid)

submission.template_fields = submission.template.fields if submitter_attrs[:completed].present? &&
submission.template_fields.blank?

Expand All @@ -54,7 +60,22 @@ def call(template:, user:, submissions_attrs:, source:, submitters_order:, param

submission.template_submitters << template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid')

is_order_sent = submitters_order == 'random' || index.zero?
# Find the position of this submitter in the original template submitters array
template_submitter_index = template.submitters.index { |s| s['uuid'] == uuid }

is_order_sent = case submitters_order
# Legacy
when 'random', 'simultaneous'
true
when 'manager_then_employee'
# Send to second party (index 1) first
template_submitter_index == 1
when 'employee_then_manager'
# Send to first party (index 0) first
template_submitter_index.zero?
else # 'preserved' Legacy
index.zero?
end

build_submitter(submission:, attrs: submitter_attrs,
uuid:, is_order_sent:, user:, params:,
Expand Down
9 changes: 6 additions & 3 deletions lib/submitters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,14 @@ def send_signature_requests(submitters, delay_seconds: nil)

def current_submitter_order?(submitter)
submitter_items = submitter.submission.template_submitters || submitter.submission.template.submitters
submitters_order = submitter.submission.template_signing_order

before_items = submitter_items[0...(submitter_items.find_index { |e| e['uuid'] == submitter.uuid })]
ordered_items = submitters_order == 'manager_then_employee' ? submitter_items.reverse : submitter_items

before_items.reduce(true) do |acc, item|
acc && submitter.submission.submitters.find { |e| e.uuid == item['uuid'] }&.completed_at?
before_items = ordered_items[0...ordered_items.find_index { |e| e['uuid'] == submitter.uuid }]

before_items.all? do |item|
submitter.submission.submitters.find { |e| e.uuid == item['uuid'] }&.completed_at?
end
end

Expand Down
68 changes: 68 additions & 0 deletions spec/lib/submissions/create_from_submitters_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Submissions::CreateFromSubmitters do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
let(:template) { create(:template, account:, author: user, submitter_count: 2) }

let(:submitter_attrs) do
template.submitters.map do |s|
HashWithIndifferentAccess.new({ 'uuid' => s['uuid'], 'email' => Faker::Internet.email })
end
end

def call(template:, submitters_order:)
described_class.call(
template:,
user:,
submissions_attrs: [HashWithIndifferentAccess.new({ 'submitters' => submitter_attrs })],
source: :api,
submitters_order:
)
end

describe 'is_order_sent for employee_then_manager' do
it 'sets sent_at only on the first submitter (Employee)' do
submissions = call(template:, submitters_order: 'employee_then_manager')
submitters = submissions.first.submitters.sort_by { |s| template.submitters.index { |ts| ts['uuid'] == s.uuid } }

expect(submitters[0].sent_at).not_to be_nil
expect(submitters[1].sent_at).to be_nil
end
end

describe 'is_order_sent for manager_then_employee' do
it 'sets sent_at only on the second submitter (Manager)' do
submissions = call(template:, submitters_order: 'manager_then_employee')
submitters = submissions.first.submitters.sort_by { |s| template.submitters.index { |ts| ts['uuid'] == s.uuid } }

expect(submitters[0].sent_at).to be_nil
expect(submitters[1].sent_at).not_to be_nil
end
end

describe 'is_order_sent for simultaneous' do
it 'sets sent_at on all submitters' do
submissions = call(template:, submitters_order: 'simultaneous')

expect(submissions.first.submitters).to all(have_attributes(sent_at: be_present))
end
end

describe 'single_sided skipping' do
before do
manager_uuid = template.submitters[1]['uuid']
template.update_column(:fields, template.fields.reject { |f| f['submitter_uuid'] == manager_uuid })
end

it 'skips submitters without fields' do
submissions = call(template:, submitters_order: 'single_sided')
submitter_uuids = submissions.first.submitters.map(&:uuid)

expect(submitter_uuids).to include(template.submitters[0]['uuid'])
expect(submitter_uuids).not_to include(template.submitters[1]['uuid'])
end
end
end
Loading
Loading