diff --git a/Gemfile b/Gemfile index 63efe6c7..c967bd8f 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem 'devise-two-factor' gem 'dotenv', require: false gem 'email_typo' gem 'faraday' +gem 'faraday-follow_redirects' gem 'google-cloud-storage', require: false gem 'hexapdf' gem 'image_processing' diff --git a/Gemfile.lock b/Gemfile.lock index d07463b2..9ab1ed46 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -189,6 +189,8 @@ GEM faraday-em_http (1.0.0) faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) @@ -580,6 +582,7 @@ DEPENDENCIES factory_bot_rails faker faraday + faraday-follow_redirects google-cloud-storage hexapdf image_processing diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index c3000285..36847423 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -2,9 +2,6 @@ module Api class SubmissionsController < ApiBaseController - UnknownFieldName = Class.new(StandardError) - UnknownSubmitterName = Class.new(StandardError) - load_and_authorize_resource :template before_action do @@ -12,68 +9,83 @@ module Api end def create + is_send_email = !params[:send_email].in?(['false', false]) + submissions = if (emails = (params[:emails] || params[:email]).presence) Submissions.create_from_emails(template: @template, user: current_user, source: :api, - mark_as_sent: params[:send_email] != 'false', + mark_as_sent: is_send_email, emails:) else - submissions_attrs = normalize_submissions_params!(submissions_params[:submission], @template) + submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template) Submissions.create_from_submitters( template: @template, user: current_user, source: :api, - mark_as_sent: params[:send_email] != 'false', + mark_as_sent: is_send_email, submitters_order: params[:submitters_order] || 'preserved', submissions_attrs: ) end - Submissions.send_signature_requests(submissions, send_email: params[:send_email] != 'false') + Submissions.send_signature_requests(submissions, send_email: is_send_email) + + submitters = submissions.flat_map(&:submitters) - render json: submissions.flat_map(&:submitters) - rescue UnknownFieldName, UnknownSubmitterName => e + save_default_value_attachments!(attachments, submitters) + + render json: submitters + rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e render json: { error: e.message }, status: :unprocessable_entity end private def submissions_params - params.permit(submission: [{ submitters: [[:uuid, :name, :email, :role, :phone, { values: {} }]] }]) + params.permit(submission: [{ + submitters: [[:uuid, :name, :email, :role, :completed, :phone, { values: {} }]] + }]) end def normalize_submissions_params!(submissions_params, template) - submissions_params.each do |submission| + attachments = [] + + Array.wrap(submissions_params).each do |submission| submission[:submitters].each_with_index do |submitter, index| next if submitter[:values].blank? - submitter[:values] = - normalize_submitter_values(template, - submitter[:values], - submitter[:role] || template.submitters[index]['name']) + values, new_attachments = + Submitters::NormalizeValues.call(template, + submitter[:values], + submitter[:role] || template.submitters[index]['name']) + + attachments.push(*new_attachments) + + submitter[:values] = values end end - submissions_params + [submissions_params, attachments] end - def normalize_submitter_values(template, values, submitter_name) - submitter = - template.submitters.find { |e| e['name'] == submitter_name } || - raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}") + def save_default_value_attachments!(attachments, submitters) + return if attachments.blank? - fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] } + attachments_index = attachments.index_by(&:uuid) - fields_uuid_index = fields.index_by { |e| e['uuid'] } - fields_name_index = fields.index_by { |e| e['name'] } + submitters.each do |submitter| + submitter.values.to_a.each do |_, value| + attachment = attachments_index[value] - values.transform_keys do |key| - next key if fields_uuid_index[key].present? + next unless attachment - fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}") + attachment.record = submitter + + attachment.save! + end end end end diff --git a/lib/submissions.rb b/lib/submissions.rb index 677466e8..190d6f8a 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -42,33 +42,9 @@ module Submissions def create_from_submitters(template:, user:, submissions_attrs:, source:, mark_as_sent: false, submitters_order: DEFAULT_SUBMITTERS_ORDER) - submissions_attrs.map do |attrs| - submission = template.submissions.new(created_by_user: user, source:, - template_submitters: template.submitters, submitters_order:) - - attrs[:submitters].each_with_index do |submitter_attrs, index| - uuid = - submitter_attrs[:uuid].presence || - template.submitters.find { |e| e['name'] == submitter_attrs[:role] }&.dig('uuid') || - template.submitters[index]&.dig('uuid') - - next if uuid.blank? - - is_order_sent = submitters_order == 'random' || index.zero? - email = normalize_email(submitter_attrs[:email]) - - submission.submitters.new( - email:, - phone: submitter_attrs[:phone].to_s.gsub(/[^0-9+]/, ''), - name: submitter_attrs[:name], - sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil, - values: submitter_attrs[:values] || {}, - uuid: - ) - end - - submission.tap(&:save!) - end + Submissions::CreateFromSubmitters.call( + template:, user:, submissions_attrs:, source:, mark_as_sent:, submitters_order: + ) end def send_signature_requests(submissions, params) diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb new file mode 100644 index 00000000..60a0b43c --- /dev/null +++ b/lib/submissions/create_from_submitters.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Submissions + module CreateFromSubmitters + module_function + + def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false) + Array.wrap(submissions_attrs).map do |attrs| + submission = template.submissions.new(created_by_user: user, source:, + template_submitters: template.submitters, submitters_order:) + + attrs[:submitters].each_with_index do |submitter_attrs, index| + uuid = + submitter_attrs[:uuid].presence || + template.submitters.find { |e| e['name'] == submitter_attrs[:role] }&.dig('uuid') || + template.submitters[index]&.dig('uuid') + + next if uuid.blank? + + is_order_sent = submitters_order == 'random' || index.zero? + + build_submitter(submission:, attrs: submitter_attrs, uuid:, is_order_sent:, mark_as_sent:) + end + + submission.tap(&:save!) + end + end + + def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:) + email = Submissions.normalize_email(attrs[:email]) + + submission.submitters.new( + email:, + phone: attrs[:phone].to_s.gsub(/[^0-9+]/, ''), + name: attrs[:name], + completed_at: attrs[:completed] ? Time.current : nil, + sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil, + values: attrs[:values] || {}, + uuid: + ) + end + end +end diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb new file mode 100644 index 00000000..86f35ab0 --- /dev/null +++ b/lib/submitters/normalize_values.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Submitters + module NormalizeValues + CHECKSUM_CACHE_STORE = ActiveSupport::Cache::MemoryStore.new + + UnknownFieldName = Class.new(StandardError) + UnknownSubmitterName = Class.new(StandardError) + + module_function + + def call(template, values, submitter_name) + submitter = + template.submitters.find { |e| e['name'] == submitter_name } || + raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}") + + fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] } + + fields_uuid_index = fields.index_by { |e| e['uuid'] } + fields_name_index = fields.index_by { |e| e['name'] } + + attachments = [] + + normalized_values = values.to_h.to_h do |key, value| + if fields_uuid_index[key].blank? + key = fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}") + end + + if fields_uuid_index[key]['type'].in?(%w[initials signature image file]) + new_value, new_attachments = normalize_attachment_value(value, template.account) + + attachments.push(*new_attachments) + + value = new_value + end + + [key, value] + end + + [normalized_values, attachments] + end + + def normalize_attachment_value(value, account) + if value.is_a?(Array) + new_attachments = value.map { |v| build_attachment(v, account) } + + [new_attachments.map(&:uuid), new_attachments] + else + new_attachment = build_attachment(value, account) + + [new_attachment.uuid, new_attachment] + end + end + + def build_attachment(value, account) + ActiveStorage::Attachment.new( + blob: find_or_create_blobs(account, value), + name: 'attachments' + ) + end + + def find_or_create_blobs(account, url) + cache_key = [account.id, url].join(':') + checksum = CHECKSUM_CACHE_STORE.fetch(cache_key) + + blob = find_blob_by_checksum(checksum, account) if checksum + + return blob if blob + + data = conn.get(url).body + + checksum = Digest::MD5.base64digest(data) + + CHECKSUM_CACHE_STORE.write(cache_key, checksum) + + blob = find_blob_by_checksum(checksum, account) + + blob || ActiveStorage::Blob.create_and_upload!( + io: StringIO.new(data), + filename: Addressable::URI.parse(url).path.split('/').last + ) + end + + def find_blob_by_checksum(checksum, account) + ActiveStorage::Blob + .joins('JOIN active_storage_attachments ON active_storage_attachments.blob_id = active_storage_blobs.id') + .where(active_storage_attachments: { record_id: account.submitters.select(:id), + record_type: 'Submitter' }) + .find_by(checksum:) + end + + def conn + Faraday.new do |faraday| + faraday.response :follow_redirects + end + end + end +end