Files
docuseal/lib/submitters/submit_values.rb
T

514 lines
17 KiB
Ruby
Raw Normal View History

2023-08-12 12:34:02 +03:00
# frozen_string_literal: true
module Submitters
module SubmitValues
2023-09-05 01:00:21 +03:00
ValidationError = Class.new(StandardError)
2025-03-28 21:36:26 +02:00
RequiredFieldError = Class.new(StandardError)
2023-09-05 01:00:21 +03:00
2023-11-05 01:41:54 +02:00
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
2026-02-20 12:27:39 +02:00
PHONE_REGEXP = /[+\d()\s-]+/
2025-09-26 20:52:44 +03:00
NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze
2023-11-05 01:41:54 +02:00
2025-08-07 11:12:26 +03:00
STRFTIME_MAP = {
'hour' => '%-k',
'minute' => '%M',
'day' => '%-d',
'month' => '%-m',
'year' => '%Y'
}.freeze
2023-08-12 12:34:02 +03:00
module_function
2025-03-29 21:12:22 +02:00
def call(submitter, params, request, validate_required: true)
2023-09-14 20:31:07 +03:00
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
2023-09-10 12:55:41 +03:00
2023-09-14 20:31:07 +03:00
unless submitter.submission_events.exists?(event_type: 'start_form')
2023-09-10 12:55:41 +03:00
SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request)
2023-09-14 20:31:07 +03:00
2025-07-04 21:11:08 +03:00
WebhookUrls.enqueue_events(submitter, 'form.started')
2023-09-10 12:55:41 +03:00
end
2023-08-12 12:34:02 +03:00
2025-03-29 21:12:22 +02:00
update_submitter!(submitter, params, request, validate_required:)
2023-09-05 01:00:21 +03:00
2023-08-12 12:34:02 +03:00
submitter.submission.save!
2026-07-01 18:10:08 +03:00
if submitter.completed_at?
is_last = Submissions.maybe_update_completed_at(submitter.submission)
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'is_last' => is_last)
end
2023-08-12 12:34:02 +03:00
submitter
end
2025-03-29 21:12:22 +02:00
def update_submitter!(submitter, params, request, validate_required: true)
2023-09-05 01:00:21 +03:00
values = normalized_values(params)
submitter.values.merge!(values)
2023-08-12 12:34:02 +03:00
submitter.opened_at ||= Time.current
2025-03-29 21:12:22 +02:00
assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true'
2023-08-17 10:33:53 +03:00
2023-09-27 23:28:33 +03:00
ApplicationRecord.transaction do
2026-02-17 10:04:11 +02:00
reason_field = maybe_set_signature_reason!(values, submitter, params)
validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request)
2023-09-27 23:28:33 +03:00
2026-02-02 12:46:34 +02:00
if (touch_attachment_uuid = params[:touch_attachment_uuid].presence)
ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at)
end
2026-02-20 12:27:39 +02:00
if params[:completed] == 'true'
maybe_invite_via_field(submitter, request)
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request)
end
2023-09-27 23:28:33 +03:00
submitter.save!
end
2023-08-12 12:34:02 +03:00
2025-06-05 13:16:55 +03:00
SearchEntries.enqueue_reindex(submitter) if submitter.completed_at?
2023-08-12 12:34:02 +03:00
submitter
end
2025-03-29 21:12:22 +02:00
def assign_completed_attributes(submitter, request, validate_required: true)
2024-06-11 18:19:21 +03:00
submitter.completed_at = Time.current
submitter.ip = request.remote_ip
submitter.ua = request.user_agent
2025-06-13 19:12:35 +03:00
submitter.timezone = request.params[:timezone]
2025-02-20 11:59:29 +02:00
2024-06-11 18:19:21 +03:00
submitter.values = merge_default_values(submitter)
2025-03-28 21:36:26 +02:00
required_field_uuids_acc = Set.new
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
2025-02-20 11:59:29 +02:00
formula_values = build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
2025-03-28 21:36:26 +02:00
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
2025-02-20 11:59:29 +02:00
end
2026-04-23 20:25:41 +03:00
submitter.values = replace_current_date_placeholders(submitter)
2024-06-11 18:19:21 +03:00
2025-03-28 21:36:26 +02:00
required_field_uuids_acc.each do |uuid|
2025-03-29 21:12:22 +02:00
next if submitter.values[uuid].present?
raise RequiredFieldError, uuid if validate_required
Rollbar.warning("Required field #{submitter.id}: #{uuid}") if defined?(Rollbar)
2025-03-28 21:36:26 +02:00
end
2024-06-11 18:19:21 +03:00
submitter
end
2024-08-06 00:19:33 +03:00
def maybe_set_signature_reason!(values, submitter, params)
return if params[:with_reason].blank?
reason_field_uuid = params[:with_reason]
signature_field_uuid = values.except(reason_field_uuid).keys.first
2026-04-13 18:22:25 +03:00
signature_field = submitter.submission.template_fields.find do |e|
e['uuid'] == signature_field_uuid && e['submitter_uuid'] == submitter.uuid
end
2024-08-06 00:19:33 +03:00
2026-04-13 18:22:25 +03:00
reason_field = submitter.submission.template_fields.find do |e|
e['uuid'] == reason_field_uuid && e['submitter_uuid'] == submitter.uuid
end
2026-02-22 12:15:44 +02:00
2026-04-13 18:22:25 +03:00
if reason_field
if reason_field.dig('preferences', 'signature_field_uuid') != signature_field['uuid']
raise ValidationError, 'Invalid field'
end
else
2024-08-06 00:19:33 +03:00
reason_field = { 'type' => 'text',
'uuid' => reason_field_uuid,
'name' => I18n.t(:reason),
'readonly' => true,
2026-04-13 18:22:25 +03:00
'preferences' => { 'signature_field_uuid' => signature_field['uuid'] },
2024-08-06 00:19:33 +03:00
'submitter_uuid' => submitter.uuid }
submitter.submission.template_fields.insert(submitter.submission.template_fields.index(signature_field) + 1,
reason_field)
end
2026-04-13 18:22:25 +03:00
signature_field['preferences'] ||= {}
signature_field['preferences']['reason_field_uuid'] = reason_field_uuid
2024-08-06 00:19:33 +03:00
submitter.submission.save!
2026-02-17 10:04:11 +02:00
reason_field
2024-08-06 00:19:33 +03:00
end
2023-08-12 12:34:02 +03:00
def normalized_values(params)
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
if params[:cast_boolean] == 'true'
v == 'true'
2024-02-23 15:58:49 +02:00
elsif params[:cast_number] == 'true'
2024-11-17 21:29:20 +02:00
if v == ''
nil
else
(v.to_f % 1).zero? ? v.to_i : v.to_f
end
2023-09-05 01:00:21 +03:00
elsif params[:normalize_phone] == 'true'
v.to_s.gsub(/[^0-9+]/, '')
2023-08-12 12:34:02 +03:00
else
v.is_a?(Array) ? v.compact_blank : v
end
end
end
2023-09-05 01:00:21 +03:00
2023-09-27 23:28:33 +03:00
def validate_values!(values, submitter, params, request)
2023-09-05 01:00:21 +03:00
values.each do |key, value|
field = submitter.submission.template_fields.find { |e| e['uuid'] == key }
2023-09-27 23:28:33 +03:00
validate_value!(value, field, params, submitter, request)
2023-09-05 01:00:21 +03:00
end
end
2025-08-19 12:25:23 +03:00
def merge_default_values(submitter, with_verification: true)
2023-11-05 01:41:54 +02:00
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
next if field['submitter_uuid'] != submitter.uuid
2023-12-22 13:33:53 +02:00
if field['type'] == 'stamp'
2024-03-14 14:09:43 +02:00
acc[field['uuid']] ||=
2024-06-13 22:57:29 +03:00
Submitters::CreateStampAttachment.build_attachment(
submitter,
with_logo: field.dig('preferences', 'with_logo') != false
).uuid
2023-12-22 13:33:53 +02:00
next
end
2025-08-19 12:25:23 +03:00
if field['type'] == 'verification' && with_verification
2024-12-01 00:10:07 +02:00
acc[field['uuid']] =
if submitter.submission_events.exists?(event_type: :complete_verification)
I18n.t(:verified, locale: :en)
elsif field['required']
raise ValidationError, 'ID Not Verified'
end
next
end
2023-11-05 01:41:54 +02:00
value = field['default_value']
next if value.blank?
2026-04-23 20:25:41 +03:00
acc[field['uuid']] = template_default_value_for_submitter(value, submitter, field:, with_time: true)
2023-11-05 01:41:54 +02:00
end
default_values.compact_blank.merge(submitter.values)
end
2025-02-20 11:59:29 +02:00
def build_formula_values(submitter)
2025-04-25 12:14:49 +03:00
submission_values = nil
2024-02-24 01:01:36 +02:00
computed_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
next if field['submitter_uuid'] != submitter.uuid
2024-07-31 22:56:18 +03:00
next if field['type'] == 'payment'
2024-02-24 01:01:36 +02:00
formula = field.dig('preferences', 'formula')
next if formula.blank?
2025-04-25 12:14:49 +03:00
submission_values ||=
if submitter.submission.template_submitters.size > 1
merge_submitters_values(submitter)
else
submitter.values
end
2026-07-30 10:43:59 +03:00
values = submission_values.merge(acc.compact_blank)
2025-09-25 14:00:51 +03:00
2026-07-30 10:43:59 +03:00
acc[field['uuid']] =
if field['type'] == 'text'
eval_text_formula_value(formula, values, submitter.submission)
else
normalized_formula = normalize_formula(formula, submitter.submission, submission_values:)
calculate_formula_value(normalized_formula, values)
end
2024-02-24 01:01:36 +02:00
end
2025-02-20 11:59:29 +02:00
computed_values.compact_blank
2024-02-24 01:01:36 +02:00
end
2025-09-25 14:00:51 +03:00
def normalize_formula(formula, submission, depth: 0, submission_values: nil)
2025-08-21 13:59:44 +03:00
raise ValidationError, 'Formula infinite loop' if depth > 10
formula.gsub(/{{(.*?)}}/) do |match|
uuid = Regexp.last_match(1)
if (nested_formula = submission.fields_uuid_index.dig(uuid, 'preferences', 'formula').presence)
2025-09-25 14:00:51 +03:00
if check_field_conditions(submission_values, submission.fields_uuid_index[uuid], submission.fields_uuid_index)
"(#{normalize_formula(nested_formula, submission, depth: depth + 1, submission_values:)})"
else
'0'
end
2025-08-21 13:59:44 +03:00
else
match
end
end
end
2024-02-24 01:01:36 +02:00
def calculate_formula_value(_formula, _values)
0
end
2026-07-30 10:43:59 +03:00
def eval_text_formula_value(_formula, _values, _submission)
''
end
2026-04-23 20:25:41 +03:00
def replace_current_date_placeholders(submitter)
submitter.values.each_with_object({}) do |(uuid, v), acc|
acc[uuid] =
if v == '{{date}}'
field = submitter.submission.fields_uuid_index[uuid]
TimeUtils.current_date_value(field&.dig('preferences', 'format'), submitter.account.timezone)
else
v
end
end
end
def template_default_value_for_submitter(value, submitter, with_time: false, field: nil)
2023-11-05 01:41:54 +02:00
return if value.blank?
2023-11-15 16:19:00 +02:00
return if submitter.blank?
2023-11-05 01:41:54 +02:00
role = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
replace_default_variables(value,
submitter.attributes.merge('role' => role),
2024-03-31 18:20:20 +03:00
submitter.submission,
2026-04-23 20:25:41 +03:00
with_time:,
field:)
2023-11-05 01:41:54 +02:00
end
2025-03-28 21:36:26 +02:00
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
2025-03-29 21:12:22 +02:00
submission = submitter.submission
2025-03-28 21:36:26 +02:00
submitters_values = nil
2025-03-29 21:12:22 +02:00
has_other_submitters = submission.template_submitters.size > 1
2025-06-27 12:49:25 +03:00
has_document_conditions = submission_has_document_conditions?(submission)
2024-12-07 01:35:59 +02:00
2025-03-28 21:36:26 +02:00
attachments_index =
if has_document_conditions
2026-07-17 00:05:34 +03:00
submitters_values = merge_submitters_values(submitter)
Submissions.filtered_conditions_schema(submission, values: submitters_values)
.index_by { |i| i['attachment_uuid'] }
2025-03-28 21:36:26 +02:00
end
2025-02-02 21:36:38 +02:00
2025-03-29 21:12:22 +02:00
submission.template_fields.each do |field|
2024-06-08 12:32:41 +03:00
next if field['submitter_uuid'] != submitter.uuid
2025-03-28 21:36:26 +02:00
required_field_uuids_acc.add(field['uuid']) if required_field_uuids_acc && required_editable_field?(field)
2025-02-02 21:36:38 +02:00
2025-03-29 21:12:22 +02:00
if has_document_conditions && !check_field_areas_attachments(field, attachments_index)
2026-07-17 00:05:34 +03:00
delete_field_value!(field, submitter, submitters_values, required_field_uuids_acc)
2025-02-02 21:36:38 +02:00
end
2025-03-28 21:36:26 +02:00
if has_other_submitters && !submitters_values &&
2025-04-25 12:14:49 +03:00
field_conditions_other_submitter?(submitter, field, submission.fields_uuid_index)
2025-03-28 21:36:26 +02:00
submitters_values = merge_submitters_values(submitter)
end
2024-12-07 01:35:59 +02:00
2025-04-25 12:14:49 +03:00
unless check_field_conditions(submitters_values || submitter.values, field, submission.fields_uuid_index)
2026-07-17 00:05:34 +03:00
delete_field_value!(field, submitter, submitters_values, required_field_uuids_acc)
2024-12-07 01:35:59 +02:00
end
2024-06-08 12:32:41 +03:00
end
submitter.values
end
2026-07-17 00:05:34 +03:00
def delete_field_value!(field, submitter, submitters_values = nil, required_field_uuids_acc = nil)
submitter.values.delete(field['uuid'])
submitters_values&.delete(field['uuid'])
required_field_uuids_acc&.delete(field['uuid'])
end
2025-06-27 12:49:25 +03:00
def submission_has_document_conditions?(submission)
(submission.template_schema || submission.template.schema).any? { |e| e['conditions'].present? }
end
2025-03-28 21:36:26 +02:00
def required_editable_field?(field)
2025-03-29 21:12:22 +02:00
return false if NONEDITABLE_FIELD_TYPES.include?(field['type'])
2025-03-28 21:36:26 +02:00
field['required'].present? && field['readonly'].blank?
end
def check_field_areas_attachments(field, attachments_index)
2025-03-29 21:12:22 +02:00
return true if field['areas'].blank?
field['areas'].any? { |area| attachments_index[area['attachment_uuid']] }
2025-03-28 21:36:26 +02:00
end
def merge_submitters_values(submitter)
submitter.submission.submitters
2026-07-17 00:05:34 +03:00
.reject { |sub| sub.uuid == submitter.uuid }
2025-03-28 21:36:26 +02:00
.reduce({}) { |acc, sub| acc.merge(sub.values) }
.merge(submitter.values)
end
2025-02-02 21:36:38 +02:00
def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
2025-03-28 21:36:26 +02:00
return false if field['conditions'].blank?
2025-02-02 21:36:38 +02:00
field['conditions'].to_a.any? do |c|
fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid
end
end
2024-06-08 12:32:41 +03:00
2025-02-02 21:36:38 +02:00
def check_field_conditions(submitter_values, field, fields_uuid_index)
return true if field['conditions'].blank?
2024-06-08 12:32:41 +03:00
2024-11-27 21:47:53 +02:00
field['conditions'].each_with_object([]) do |c, acc|
if c['operation'] == 'or'
acc.push(acc.pop || check_field_condition(c, submitter_values, fields_uuid_index))
2024-06-08 12:32:41 +03:00
else
2024-11-27 21:47:53 +02:00
acc.push(check_field_condition(c, submitter_values, fields_uuid_index))
2024-06-08 12:32:41 +03:00
end
2024-11-27 21:47:53 +02:00
end.exclude?(false)
end
2026-01-31 08:39:32 +02:00
# rubocop:disable Metrics
2024-11-27 21:47:53 +02:00
def check_field_condition(condition, submitter_values, fields_uuid_index)
2026-01-15 17:23:26 +02:00
value = submitter_values[condition['field_uuid']]
2026-01-31 08:39:32 +02:00
field = fields_uuid_index[condition['field_uuid']]
2026-01-15 17:23:26 +02:00
2024-11-27 21:47:53 +02:00
case condition['action']
when 'empty', 'unchecked'
2026-01-15 17:23:26 +02:00
value.blank?
2024-11-27 21:47:53 +02:00
when 'not_empty', 'checked'
2026-01-15 17:23:26 +02:00
value.present?
2026-01-31 08:39:32 +02:00
when ->(action) { action == 'equal' && field&.dig('type') == 'number' }
return false if value.blank? || condition['value'].blank?
(value.to_f - condition['value'].to_f).abs < Float::EPSILON
when ->(action) { action == 'not_equal' && field&.dig('type') == 'number' }
return false if value.blank? || condition['value'].blank?
(value.to_f - condition['value'].to_f).abs > Float::EPSILON
when 'greater_than'
return false if field.nil? || value.blank? || condition['value'].blank?
2026-01-15 17:23:26 +02:00
2026-01-31 08:39:32 +02:00
value.to_f > condition['value'].to_f
when 'less_than'
return false if field.nil? || value.blank? || condition['value'].blank?
value.to_f < condition['value'].to_f
when 'equal', 'contains'
2026-01-15 17:23:26 +02:00
return true unless field
values = Array.wrap(value)
return values.include?(condition['value']) unless field['options']
2024-11-27 21:47:53 +02:00
option = field['options'].find { |o| o['uuid'] == condition['value'] }
2026-01-15 17:23:26 +02:00
return false unless option
2024-11-27 21:47:53 +02:00
2025-06-03 22:29:14 +03:00
values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}")
2024-11-27 21:47:53 +02:00
when 'not_equal', 'does_not_contain'
2026-01-15 17:23:26 +02:00
return true unless field
return false unless field['options']
2024-11-27 21:47:53 +02:00
option = field['options'].find { |o| o['uuid'] == condition['value'] }
2026-01-15 17:23:26 +02:00
return false unless option
values = Array.wrap(value)
2024-11-27 21:47:53 +02:00
2025-06-03 22:29:14 +03:00
values.exclude?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}")
2024-11-27 21:47:53 +02:00
else
true
2024-06-08 12:32:41 +03:00
end
end
2026-01-31 08:39:32 +02:00
# rubocop:enable Metrics
2024-06-08 12:32:41 +03:00
2026-04-23 20:25:41 +03:00
def replace_default_variables(value, attrs, submission, with_time: false, field: nil)
2025-08-23 20:11:10 +03:00
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
2023-11-05 01:41:54 +02:00
return if value.blank?
2023-12-14 21:22:26 +02:00
value.to_s.gsub(VARIABLE_REGEXP) do |e|
2023-11-05 01:41:54 +02:00
case key = ::Regexp.last_match(1)
2024-03-22 13:11:29 +02:00
when 'id'
attrs['submission_id']
2023-11-05 01:41:54 +02:00
when 'time'
if with_time
2024-03-31 18:20:20 +03:00
I18n.l(Time.current.in_time_zone(submission.account.timezone),
format: :long, locale: submission.account.locale)
2023-11-06 13:36:38 +02:00
else
e
2023-11-05 01:41:54 +02:00
end
2025-08-07 11:12:26 +03:00
when 'hour', 'minute', 'day', 'month', 'year'
with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e
2023-11-05 01:41:54 +02:00
when 'date'
2026-04-23 20:25:41 +03:00
with_time ? TimeUtils.current_date_value(field&.dig('preferences', 'format'), submission.account.timezone) : e
2023-11-06 13:36:38 +02:00
when 'role', 'email', 'phone', 'name'
2023-11-05 01:41:54 +02:00
attrs[key] || e
else
e
end
end
end
2026-02-20 12:27:39 +02:00
def maybe_invite_via_field(submitter, request)
submission = submitter.submission
is_invited = false
submission.template_submitters.each do |s|
field_uuid = s['invite_via_field_uuid']
next if field_uuid.blank?
field = submission.template_fields.find { |e| e['uuid'] == field_uuid }
next unless field
next unless field['submitter_uuid'] == submitter.uuid
next if submission.submitters.exists?(uuid: s['uuid'])
value = submitter.values[field_uuid]
next if value.blank?
if value.include?('@')
email = Submissions.normalize_email(value)
elsif value.match?(PHONE_REGEXP)
phone = value.gsub(/[^+\d]/, '')
end
next if email.blank? && phone.blank?
submission.submitters.create!(uuid: s['uuid'], email:, phone:, account_id: submitter.account_id)
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
is_invited = true
end
submission.update!(submitters_order: :preserved) if is_invited
submitter
end
2026-02-17 08:42:21 +02:00
def validate_value!(_value, field, _params, submitter, _request)
2026-04-13 19:55:07 +03:00
raise ValidationError, 'Missing field' unless field
raise ValidationError, 'Invalid field' if field['submitter_uuid'] != submitter.uuid
2026-04-13 16:32:03 +03:00
2026-02-17 08:42:21 +02:00
if field['readonly'] == true
Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar)
raise ValidationError, 'Read-only field'
end
2023-09-05 01:00:21 +03:00
true
end
2023-08-12 12:34:02 +03:00
end
end