diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 8f61477f..0424e635 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -50,35 +50,14 @@ module Api end def create - is_send_email = !params[:send_email].in?(['false', false]) - - submissions = - if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank? - Submissions.create_from_emails(template: @template, - user: current_user, - source: :api, - mark_as_sent: is_send_email, - emails:) - else - submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template) - - Submissions.create_from_submitters( - template: @template, - user: current_user, - source: :api, - mark_as_sent: is_send_email, - submitters_order: params[:submitters_order] || params[:order] || 'preserved', - submissions_attrs: - ) - end + params[:send_email] = true unless params.key?(:send_email) + params[:send_sms] = false unless params.key?(:send_sms) - Submissions.send_signature_requests(submissions, send_email: is_send_email) + submissions = create_submissions(@template, params) - submitters = submissions.flat_map(&:submitters) + Submissions.send_signature_requests(submissions) - save_default_value_attachments!(attachments, submitters) - - render json: submitters + render json: submissions.flat_map(&:submitters) rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e render json: { error: e.message }, status: :unprocessable_entity end @@ -91,6 +70,35 @@ module Api private + def create_submissions(template, params) + is_send_email = !params[:send_email].in?(['false', false]) + + if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank? + Submissions.create_from_emails(template:, + user: current_user, + source: :api, + mark_as_sent: is_send_email, + emails:, + params:) + else + submissions_attrs, attachments = normalize_submissions_params!(submissions_params, template) + + submissions = Submissions.create_from_submitters( + template:, + user: current_user, + source: :api, + mark_as_sent: is_send_email, + submitters_order: params[:submitters_order] || params[:order] || 'preserved', + submissions_attrs:, + params: + ) + + save_default_value_attachments!(attachments, submissions.flat_map(&:submitters)) + + submissions + end + end + def serialize_params { only: %i[id source submitters_order created_at updated_at], @@ -107,11 +115,19 @@ module Api end def submissions_params - params.permit(submission: [{ - submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key, - { values: {}, readonly_fields: [], - fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] - }]) + key = params.key?(:submission) ? :submission : :submissions + + params.permit( + key => [ + [:send_email, :send_sms, { + message: %i[subject body], + submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role, + :completed, :phone, :application_key, + { values: {}, readonly_fields: [], message: %i[subject body], + fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] + }] + ] + ).fetch(key, []) end def normalize_submissions_params!(submissions_params, template) diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index ca054121..e4e493bb 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -23,7 +23,8 @@ class StartFormController < ApplicationController @submitter.assign_attributes( uuid: @template.submitters.first['uuid'], ip: request.remote_ip, - ua: request.user_agent + ua: request.user_agent, + preferences: { 'send_email' => true } ) @submitter.submission ||= Submission.new(template: @template, diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index a327d56e..64d8df23 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -34,23 +34,30 @@ class SubmissionsController < ApplicationController def create authorize!(:create, Submission) + if params[:is_custom_message] != '1' + params.delete(:subject) + params.delete(:body) + end + submissions = if params[:emails].present? Submissions.create_from_emails(template: @template, user: current_user, source: :invite, mark_as_sent: params[:send_email] == '1', - emails: params[:emails]) + emails: params[:emails], + params:) else Submissions.create_from_submitters(template: @template, user: current_user, source: :invite, submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', mark_as_sent: params[:send_email] == '1', - submissions_attrs: submissions_params[:submission].to_h.values) + submissions_attrs: submissions_params[:submission].to_h.values, + params:) end - Submissions.send_signature_requests(submissions, params) + Submissions.send_signature_requests(submissions) redirect_to template_path(@template), notice: 'New recipients have been added' end diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index e481a74e..f8ebc058 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -31,7 +31,8 @@ class ProcessSubmitterCompletionJob < ApplicationJob SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later! end - to = submitter.submission.submitters.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ') + to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false } + .sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ') SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present? end @@ -48,6 +49,6 @@ class ProcessSubmitterCompletionJob < ApplicationJob next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] } - Submitters.send_signature_requests([next_submitter], send_email: true) + Submitters.send_signature_requests([next_submitter]) end end diff --git a/app/jobs/send_submitter_invitation_email_job.rb b/app/jobs/send_submitter_invitation_email_job.rb index 4036df16..b57f54db 100644 --- a/app/jobs/send_submitter_invitation_email_job.rb +++ b/app/jobs/send_submitter_invitation_email_job.rb @@ -4,7 +4,7 @@ class SendSubmitterInvitationEmailJob < ApplicationJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) - SubmitterMailer.invitation_email(submitter, subject: params['subject'], body: params['body']).deliver_now! + SubmitterMailer.invitation_email(submitter).deliver_now! SubmissionEvent.create!(submitter:, event_type: 'send_email') diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index 80e43313..68c9bcf5 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -3,18 +3,26 @@ class SubmitterMailer < ApplicationMailer MAX_ATTACHMENTS_SIZE = 10.megabytes - def invitation_email(submitter, body: nil, subject: nil) + DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form' + + def invitation_email(submitter) @current_account = submitter.submission.template.account @submitter = submitter - @body = body.presence + + if submitter.preferences['email_message_uuid'] + @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) + end + + @body = @email_message&.body.presence + @subject = @email_message&.subject.presence @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) subject = - if @email_config || subject.present? - ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:) + if @email_config || @subject + ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:) else - 'You are invited to submit a form' + DEFAULT_INVITATION_SUBJECT end mail(to: @submitter.friendly_name, diff --git a/app/models/account.rb b/app/models/account.rb index a9659b21..8c2792e9 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -15,6 +15,7 @@ class Account < ApplicationRecord has_many :users, dependent: :destroy has_many :encrypted_configs, dependent: :destroy has_many :account_configs, dependent: :destroy + has_many :email_messages, dependent: :destroy has_many :templates, dependent: :destroy has_many :template_folders, dependent: :destroy has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) }, diff --git a/app/models/email_message.rb b/app/models/email_message.rb new file mode 100644 index 00000000..25156d6f --- /dev/null +++ b/app/models/email_message.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: email_messages +# +# id :bigint not null, primary key +# body :text not null +# sha1 :string not null +# subject :text not null +# uuid :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# author_id :bigint not null +# +# Indexes +# +# index_email_messages_on_account_id (account_id) +# index_email_messages_on_sha1 (sha1) +# index_email_messages_on_uuid (uuid) +# +# Foreign Keys +# +# fk_rails_... (account_id => accounts.id) +# fk_rails_... (author_id => users.id) +# +class EmailMessage < ApplicationRecord + belongs_to :author, class_name: 'User' + belongs_to :account + + attribute :uuid, :string, default: -> { SecureRandom.uuid } + + before_validation :set_sha1, on: :create + + def set_sha1 + self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json) + end +end diff --git a/app/models/submission.rb b/app/models/submission.rb index e240c774..48ec202c 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # deleted_at :datetime +# preferences :text not null # slug :string not null # source :text not null # submitters_order :string not null @@ -36,9 +37,12 @@ class Submission < ApplicationRecord has_many :submitters, dependent: :destroy has_many :submission_events, dependent: :destroy + attribute :preferences, :string, default: -> { {} } + serialize :template_fields, JSON serialize :template_schema, JSON serialize :template_submitters, JSON + serialize :preferences, JSON attribute :source, :string, default: 'link' attribute :submitters_order, :string, default: 'random' diff --git a/app/models/submitter.rb b/app/models/submitter.rb index 36c21508..f5065d7e 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -12,6 +12,7 @@ # name :string # opened_at :datetime # phone :string +# preferences :text not null # sent_at :datetime # slug :string not null # ua :string @@ -37,9 +38,11 @@ class Submitter < ApplicationRecord has_one :account, through: :template attribute :values, :string, default: -> { {} } + attribute :preferences, :string, default: -> { {} } attribute :slug, :string, default: -> { SecureRandom.base58(14) } serialize :values, JSON + serialize :preferences, JSON has_many_attached :documents has_many_attached :attachments diff --git a/app/models/user.rb b/app/models/user.rb index d8f23419..92129293 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -55,6 +55,7 @@ class User < ApplicationRecord has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :user_configs, dependent: :destroy has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig' + has_many :email_messages, dependent: :destroy devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant? diff --git a/db/migrate/20231122203341_add_preferences_to_submitters.rb b/db/migrate/20231122203341_add_preferences_to_submitters.rb new file mode 100644 index 00000000..45b9bc87 --- /dev/null +++ b/db/migrate/20231122203341_add_preferences_to_submitters.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddPreferencesToSubmitters < ActiveRecord::Migration[7.0] + class MigrationSubmitter < ApplicationRecord + self.table_name = 'submitters' + end + + def change + add_column :submitters, :preferences, :text + + MigrationSubmitter.where(preferences: nil).update_all(preferences: '{}') + + change_column_null :submitters, :preferences, false + end +end diff --git a/db/migrate/20231122203347_add_preferences_to_submissions.rb b/db/migrate/20231122203347_add_preferences_to_submissions.rb new file mode 100644 index 00000000..5621845b --- /dev/null +++ b/db/migrate/20231122203347_add_preferences_to_submissions.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddPreferencesToSubmissions < ActiveRecord::Migration[7.0] + class MigrationSubmission < ApplicationRecord + self.table_name = 'submissions' + end + + def change + add_column :submissions, :preferences, :text + + MigrationSubmission.where(preferences: nil).update_all(preferences: '{}') + + change_column_null :submissions, :preferences, false + end +end diff --git a/db/migrate/20231122212612_create_email_messages.rb b/db/migrate/20231122212612_create_email_messages.rb new file mode 100644 index 00000000..e3efb23f --- /dev/null +++ b/db/migrate/20231122212612_create_email_messages.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateEmailMessages < ActiveRecord::Migration[7.0] + def change + create_table :email_messages do |t| + t.string :uuid, null: false, index: true + t.references :author, null: false, foreign_key: { to_table: :users }, index: false + t.references :account, null: false, foreign_key: true, index: true + t.text :subject, null: false + t.text :body, null: false + t.string :sha1, null: false, index: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b01abe1f..c518087d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do +ActiveRecord::Schema[7.0].define(version: 2023_11_22_212612) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -81,6 +81,20 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id" end + create_table "email_messages", force: :cascade do |t| + t.string "uuid", null: false + t.bigint "author_id", null: false + t.bigint "account_id", null: false + t.text "subject", null: false + t.text "body", null: false + t.string "sha1", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_email_messages_on_account_id" + t.index ["sha1"], name: "index_email_messages_on_sha1" + t.index ["uuid"], name: "index_email_messages_on_uuid" + end + create_table "encrypted_configs", force: :cascade do |t| t.bigint "account_id", null: false t.string "key", null: false @@ -125,6 +139,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do t.text "source", null: false t.string "submitters_order", null: false t.string "slug", null: false + t.text "preferences", null: false t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id" t.index ["slug"], name: "index_submissions_on_slug", unique: true t.index ["template_id"], name: "index_submissions_on_template_id" @@ -146,6 +161,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do t.string "name" t.string "phone" t.string "application_key" + t.text "preferences", null: false t.index ["email"], name: "index_submitters_on_email" t.index ["slug"], name: "index_submitters_on_slug", unique: true t.index ["submission_id"], name: "index_submitters_on_submission_id" @@ -229,6 +245,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "document_generation_events", "submitters" + add_foreign_key "email_messages", "accounts" + add_foreign_key "email_messages", "users", column: "author_id" add_foreign_key "encrypted_configs", "accounts" add_foreign_key "encrypted_user_configs", "users" add_foreign_key "submission_events", "submissions" diff --git a/lib/email_messages.rb b/lib/email_messages.rb new file mode 100644 index 00000000..49e9de12 --- /dev/null +++ b/lib/email_messages.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module EmailMessages + module_function + + def find_or_create_for_account_user(account, user, subject, body) + subject = SubmitterMailer::DEFAULT_INVITATION_SUBJECT if subject.blank? + + sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json) + + message = account.email_messages.find_by(sha1:) + + message ||= account.email_messages.create!(author: user, subject:, body:) + + message + end +end diff --git a/lib/submissions.rb b/lib/submissions.rb index 70aa41d1..e4630fa7 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -27,11 +27,17 @@ module Submissions submission.save! end - def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false) + def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {}) + preferences = Submitters.normalize_preferences(template.account, user, params) + parse_emails(emails).uniq.map do |email| - submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters) + submission = template.submissions.new(created_by_user: user, + source:, + template_submitters: template.submitters) + submission.submitters.new(email: normalize_email(email), uuid: template.submitters.first['uuid'], + preferences:, sent_at: mark_as_sent ? Time.current : nil) submission.tap(&:save!) @@ -45,13 +51,13 @@ module Submissions end def create_from_submitters(template:, user:, submissions_attrs:, source:, mark_as_sent: false, - submitters_order: DEFAULT_SUBMITTERS_ORDER) + submitters_order: DEFAULT_SUBMITTERS_ORDER, params: {}) Submissions::CreateFromSubmitters.call( - template:, user:, submissions_attrs:, source:, mark_as_sent:, submitters_order: + template:, user:, submissions_attrs:, source:, mark_as_sent:, submitters_order:, params: ) end - def send_signature_requests(submissions, params) + def send_signature_requests(submissions) submissions.each do |submission| submitters = submission.submitters.reject(&:completed_at?) @@ -59,9 +65,9 @@ module Submissions first_submitter = submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first - Submitters.send_signature_requests([first_submitter], params) + Submitters.send_signature_requests([first_submitter]) else - Submitters.send_signature_requests(submitters, params) + Submitters.send_signature_requests(submitters) end end end diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index 1edbd631..0d094b2b 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -4,7 +4,9 @@ module Submissions module CreateFromSubmitters module_function - def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false) + def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false, params: {}) + preferences = Submitters.normalize_preferences(template.account, user, params) + Array.wrap(submissions_attrs).map do |attrs| submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters, submitters_order:) @@ -18,7 +20,11 @@ module Submissions is_order_sent = submitters_order == 'random' || index.zero? - build_submitter(submission:, attrs: submitter_attrs, uuid:, is_order_sent:, mark_as_sent:) + submission_preferences = Submitters.normalize_preferences(template.account, user, attrs) + + build_submitter(submission:, attrs: submitter_attrs, uuid:, + is_order_sent:, mark_as_sent:, user:, + preferences: preferences.merge(submission_preferences)) end submission.tap(&:save!) @@ -85,8 +91,9 @@ module Submissions template.submitters[index]&.dig('uuid') end - def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:) + def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:, user:, preferences:) email = Submissions.normalize_email(attrs[:email]) + submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs) submission.submitters.new( email:, @@ -96,6 +103,7 @@ module Submissions completed_at: attrs[:completed] ? Time.current : nil, sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil, values: attrs[:values] || {}, + preferences: preferences.merge(submitter_preferences), uuid: ) end diff --git a/lib/submitters.rb b/lib/submitters.rb index 6a92df59..789b43de 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Submitters + TRUE_VALUES = ['1', 'true', true].freeze + module_function def search(submitters, keyword) @@ -43,21 +45,30 @@ module Submitters ) end - def send_signature_requests(submitters, params) - return if params[:send_email] != true && params[:send_email] != '1' + def normalize_preferences(account, user, params) + preferences = {} - submitters.each do |submitter| - next if submitter.email.blank? + message_params = params['message'].presence || params.slice('subject', 'body').presence - enqueue_invitation_email(submitter, params) + if message_params.present? + email_message = EmailMessages.find_or_create_for_account_user(account, user, + message_params['subject'], + message_params['body']) end + + preferences['email_message_uuid'] = email_message.uuid if email_message + preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email') + preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms') + + preferences end - def enqueue_invitation_email(submitter, params) - subject, body = params.values_at(:subject, :body) if params[:is_custom_message] == '1' + def send_signature_requests(submitters) + submitters.each do |submitter| + next if submitter.email.blank? + next if submitter.preferences['send_email'] == false - SendSubmitterInvitationEmailJob.perform_later('submitter_id' => submitter.id, - 'body' => body, - 'subject' => subject) + SendSubmitterInvitationEmailJob.perform_later('submitter_id' => submitter.id) + end end end