Files
docuseal/lib/submissions/generate_audit_trail.rb
T

353 lines
13 KiB
Ruby
Raw Normal View History

2023-09-10 19:25:57 +03:00
# frozen_string_literal: true
module Submissions
module GenerateAuditTrail
FONT_SIZE = 9
TEXT_COLOR = '525252'
2024-02-16 11:34:30 +02:00
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf'
2023-09-10 19:25:57 +03:00
FONT_NAME = if File.exist?(FONT_PATH)
2024-05-02 13:40:01 +03:00
'GoNotoKurrent'
2023-09-10 19:25:57 +03:00
else
'Helvetica'
end
2023-12-03 22:30:03 +02:00
VERIFIED_TEXT = 'Verified'
2023-11-01 21:32:34 +02:00
UNVERIFIED_TEXT = 'Unverified'
2023-09-10 19:25:57 +03:00
2023-12-14 21:22:26 +02:00
CURRENCY_SYMBOLS = {
'USD' => '$',
'EUR' => '€',
'GBP' => '£'
}.freeze
2024-01-15 01:45:26 +02:00
RTL_REGEXP = TextUtils::RTL_REGEXP
2024-02-16 18:59:10 +02:00
MAX_IMAGE_HEIGHT = 100
2024-01-07 14:19:21 +02:00
2024-03-25 20:02:20 +02:00
US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze
2023-09-10 19:25:57 +03:00
module_function
# rubocop:disable Metrics
def call(submission)
2024-05-25 20:57:36 +03:00
document = build_audit_trail(submission)
2024-03-31 17:40:51 +03:00
account = submission.account
2024-05-25 20:57:36 +03:00
2023-09-10 19:25:57 +03:00
pkcs = Accounts.load_signing_pkcs(account)
2023-12-02 18:14:33 +02:00
tsa_url = Accounts.load_timeserver_url(account)
2024-05-25 20:57:36 +03:00
io = StringIO.new
document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
sign_params = {
reason: sign_reason,
**Submissions::GenerateResultAttachments.build_signing_params(pkcs, tsa_url)
}
document.sign(io, **sign_params)
2024-07-09 13:15:51 +03:00
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
2024-05-25 20:57:36 +03:00
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
2024-07-09 13:15:51 +03:00
io: io.tap(&:rewind), filename: "Audit Log - #{submission.template.name}.pdf"
2024-05-25 20:57:36 +03:00
),
name: 'audit_trail',
record: submission
)
end
def build_audit_trail(submission)
account = submission.account
2023-09-10 19:25:57 +03:00
verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options)
2024-03-25 20:02:20 +02:00
page_size =
if TimeUtils.timezone_abbr(account.timezone, Time.current.beginning_of_year).in?(US_TIMEZONES)
:Letter
else
:A4
end
2023-09-10 19:25:57 +03:00
composer = HexaPDF::Composer.new(skip_page_creation: true)
2024-05-04 11:09:48 +03:00
composer.document.task(:pdfa) if FONT_NAME == 'GoNotoKurrent'
2024-05-02 13:40:01 +03:00
composer.document.config['font.map'] = {
'Helvetica' => {
none: FONT_PATH,
bold: FONT_BOLD_PATH
},
FONT_NAME => {
none: FONT_PATH,
bold: FONT_BOLD_PATH
}
}
2024-03-15 19:02:17 +02:00
composer.document.config['font.on_missing_glyph'] =
Submissions::GenerateResultAttachments.method(:on_missing_glyph).to_proc
2023-09-10 19:25:57 +03:00
divider = HexaPDF::Layout::Box.create(
margin: [0, 0, 15, 0],
border: {
width: [1, 0, 0, 0],
color: %w[hp-gray-light]
},
height: 1
)
2024-03-25 20:02:20 +02:00
composer.page_style(:default, page_size:) do |canvas, style|
2023-09-10 19:25:57 +03:00
box = canvas.context.box(:media)
canvas.save_graphics_state do
canvas.fill_color('FAF7F5')
.rectangle(0, 0, box.width, 20)
.rectangle(0, box.height - 20, box.width, 20)
.fill
end
style.frame = style.create_frame(canvas.context, 50)
end
2024-02-14 18:52:11 +02:00
composer.style(:base, font: FONT_NAME, font_size: FONT_SIZE, fill_color: TEXT_COLOR, line_spacing: 1)
2023-09-10 19:25:57 +03:00
composer.style(:link, fill_color: 'hp-blue-light', underline: true)
composer.new_page
composer.column(columns: 1) do |column|
2023-12-01 18:42:22 +02:00
add_logo(column, submission)
2023-09-10 19:25:57 +03:00
2024-04-28 14:44:33 +03:00
column.text(account.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log',
2023-09-10 19:25:57 +03:00
font_size: 16,
2024-02-14 18:52:11 +02:00
padding: [10, 0, 0, 0],
2024-02-15 22:26:05 +02:00
position: :float, text_align: :right)
2023-09-10 19:25:57 +03:00
end
composer.column(columns: 1) do |column|
2024-02-14 18:52:11 +02:00
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [15, 0, 8, 0], position: :float)
2023-12-11 21:40:13 +02:00
2024-02-02 19:29:01 +02:00
unless submission.source.in?(%w[embed api])
2023-12-11 21:40:13 +02:00
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }],
2024-02-15 22:26:05 +02:00
font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right)
2023-12-11 21:40:13 +02:00
end
2023-09-10 19:25:57 +03:00
end
composer.draw_box(divider)
last_submitter = submission.submitters.where.not(completed_at: nil).order(:completed_at).last
documents_data = Submitters.select_attachments_for_download(last_submitter).map do |document|
2024-03-05 17:16:18 +02:00
original_documents = submission.template.documents.select do |e|
e.uuid == (document.metadata['original_uuid'] || document.uuid)
end.presence
2023-09-10 19:25:57 +03:00
original_documents ||= submission.template.documents.select do |e|
2023-10-03 02:14:27 +03:00
e.image? && submission.template_schema.any? do |item|
2023-09-10 19:25:57 +03:00
item['attachment_uuid'] == e.uuid
end
end
2023-09-10 21:38:03 +03:00
link =
2024-02-18 11:26:30 +02:00
ActiveStorage::Blob.proxy_url(document.blob)
2023-09-10 21:38:03 +03:00
[
composer.document.layout.formatted_text_box(
[{ text: document.filename.to_s, link: }]
),
composer.document.layout.formatted_text_box(
[
2024-05-02 13:40:01 +03:00
{ text: "Original SHA256:\n", font: [FONT_NAME, { variant: :bold }] },
2023-09-10 21:38:03 +03:00
original_documents.map { |d| d.metadata['sha256'] || d.checksum }.join("\n"),
"\n",
2024-05-02 13:40:01 +03:00
{ text: "Result SHA256:\n", font: [FONT_NAME, { variant: :bold }] },
2023-09-10 21:38:03 +03:00
document.metadata['sha256'] || document.checksum,
"\n",
2024-05-02 13:40:01 +03:00
{ text: 'Generated at: ', font: [FONT_NAME, { variant: :bold }] },
2023-10-29 16:23:32 +02:00
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
"#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}"
2024-02-14 18:52:11 +02:00
], line_spacing: 1.3
2023-09-10 21:38:03 +03:00
)
]
2023-09-10 19:25:57 +03:00
end
if documents_data.present?
2024-02-14 18:52:11 +02:00
composer.table(documents_data, cell_style: { padding: [0, 0, 20, 0], border: { width: 0 } })
2023-09-10 19:25:57 +03:00
composer.draw_box(divider)
end
2023-09-10 19:25:57 +03:00
2023-09-12 22:36:04 +03:00
submission.template_submitters.filter_map do |item|
2023-09-10 19:25:57 +03:00
submitter = submission.submitters.find { |e| e.uuid == item['uuid'] }
2023-09-12 22:36:04 +03:00
next if submitter.blank?
2023-09-10 19:25:57 +03:00
completed_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.complete_form? } ||
SubmissionEvent.new
click_email_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.click_email? }
is_phone_verified =
2023-09-12 22:36:04 +03:00
submission.template_fields.any? do |e|
e['type'] == 'phone' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
end
2023-09-10 19:25:57 +03:00
submitter_field_counters = Hash.new { 0 }
2023-09-12 22:36:04 +03:00
info_rows = [
2023-09-10 19:25:57 +03:00
[
composer.document.layout.formatted_text_box(
[
submission.template_submitters.size > 1 && { text: "#{item['name']}\n" },
2024-05-02 13:40:01 +03:00
submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] },
2024-01-15 01:45:26 +02:00
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
2023-09-10 19:25:57 +03:00
submitter.phone && { text: "#{submitter.phone}\n" }
2024-02-14 18:52:11 +02:00
].compact_blank, line_spacing: 1.3, padding: [0, 20, 0, 0]
2023-09-12 22:36:04 +03:00
)
],
[
2023-09-10 19:25:57 +03:00
composer.document.layout.formatted_text_box(
[
2023-12-01 18:42:22 +02:00
submitter.email && click_email_event && {
text: "Email verification: #{VERIFIED_TEXT}\n"
2023-09-10 19:25:57 +03:00
},
2023-12-01 18:42:22 +02:00
submitter.phone && is_phone_verified && {
text: "Phone verification: #{VERIFIED_TEXT}\n"
2023-09-10 19:25:57 +03:00
},
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" },
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
"\n"
2024-02-14 18:52:11 +02:00
].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0]
2023-09-10 19:25:57 +03:00
)
2023-09-12 22:36:04 +03:00
]
]
composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } })
2023-10-24 00:32:09 +03:00
submission.template_fields.filter_map do |field|
next if field['submitter_uuid'] != submitter.uuid
2024-07-11 21:40:15 +03:00
next if field['type'] == 'heading'
2023-09-10 19:25:57 +03:00
2023-10-24 00:32:09 +03:00
submitter_field_counters[field['type']] += 1
2023-09-10 19:25:57 +03:00
2023-10-24 00:32:09 +03:00
value = submitter.values[field['uuid']]
2023-09-10 19:25:57 +03:00
2023-10-24 00:32:09 +03:00
next if Array.wrap(value).compact_blank.blank?
2023-09-12 22:36:04 +03:00
2024-06-04 19:34:09 +03:00
field_name = field['title'].presence || field['name'].to_s
2024-06-04 18:33:24 +03:00
2023-10-24 00:32:09 +03:00
[
composer.formatted_text_box(
[
{
2024-06-04 18:33:24 +03:00
text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence ||
2023-10-24 00:32:09 +03:00
"#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase,
font_size: 6
}
2024-01-07 14:19:21 +02:00
].compact_blank,
2024-06-04 18:33:24 +03:00
text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left,
2024-02-14 18:52:11 +02:00
line_spacing: 1.3, padding: [0, 0, 2, 0]
2023-10-24 00:32:09 +03:00
),
2023-12-22 13:33:53 +02:00
if field['type'].in?(%w[image signature initials stamp])
2023-10-24 00:32:09 +03:00
attachment = submitter.attachments.find { |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
scale = [600.0 / image.width, 600.0 / image.height].min
resized_image = image.resize([scale, 1].min)
io = StringIO.new(resized_image.write_to_buffer('.png'))
width = field['type'] == 'initials' ? 100 : 200
height = resized_image.height * (width.to_f / resized_image.width)
2024-02-16 18:59:10 +02:00
if height > MAX_IMAGE_HEIGHT
width = (MAX_IMAGE_HEIGHT / height) * width
height = MAX_IMAGE_HEIGHT
end
2024-02-14 18:52:11 +02:00
composer.image(io, width:, height:, margin: [5, 0, 10, 0])
2023-10-24 00:32:09 +03:00
composer.formatted_text_box([{ text: '' }])
2023-12-14 21:22:26 +02:00
elsif field['type'].in?(%w[file payment])
if field['type'] == 'payment'
2024-02-16 19:00:18 +02:00
unit = CURRENCY_SYMBOLS[field['preferences']['currency']] || field['preferences']['currency']
2023-12-14 21:22:26 +02:00
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
composer.formatted_text_box([{ text: "Paid #{price}\n" }], padding: [0, 0, 10, 0])
end
2023-10-24 00:32:09 +03:00
composer.formatted_text_box(
Array.wrap(value).map do |uuid|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
link =
2024-02-18 11:26:30 +02:00
ActiveStorage::Blob.proxy_url(attachment.blob)
2023-10-24 00:32:09 +03:00
{ link:, text: "#{attachment.filename}\n", style: :link }
end,
padding: [0, 0, 10, 0]
)
elsif field['type'] == 'checkbox'
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
else
2023-12-17 23:32:05 +02:00
if field['type'] == 'date'
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
end
2024-05-12 11:12:07 +03:00
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
2023-10-24 00:32:09 +03:00
value = value.join(', ') if value.is_a?(Array)
2024-01-15 01:45:26 +02:00
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
2024-02-15 22:26:05 +02:00
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
2024-01-07 14:19:21 +02:00
padding: [0, 0, 10, 0])
2023-10-24 00:32:09 +03:00
end
]
2023-09-12 22:36:04 +03:00
end
2023-09-10 19:25:57 +03:00
2023-09-12 22:36:04 +03:00
composer.draw_box(divider)
end
2023-09-10 19:25:57 +03:00
2023-09-10 21:38:03 +03:00
composer.text('Event Log', font_size: 12, padding: [10, 0, 20, 0])
2023-09-10 19:25:57 +03:00
events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event|
submitter = submission.submitters.find { |e| e.id == event.submitter_id }
[
2023-10-29 16:23:32 +02:00
"#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
"#{TimeUtils.timezone_abbr(account.timezone, event.event_timestamp)}",
2023-09-10 19:25:57 +03:00
composer.document.layout.formatted_text_box(
[
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
2024-05-02 13:40:01 +03:00
font: [FONT_NAME, { variant: :bold }] },
2023-09-10 19:25:57 +03:00
event.event_type.include?('send_') ? ' to ' : ' by ',
2023-09-27 23:28:33 +03:00
if event.event_type.include?('sms') || event.event_type.include?('phone')
2024-04-21 17:00:45 +03:00
event.data['phone'] || submitter.phone
2023-09-10 19:25:57 +03:00
else
2023-12-21 23:29:06 +02:00
submitter.name || submitter.email || submitter.phone
2023-09-10 19:25:57 +03:00
end
]
)
]
end
2024-02-14 18:52:11 +02:00
composer.table(events_data, cell_style: { padding: [0, 0, 12, 0], border: { width: 0 } }) if events_data.present?
2023-09-10 19:25:57 +03:00
2024-05-25 20:57:36 +03:00
composer.document
end
2023-09-10 19:25:57 +03:00
2024-05-25 20:57:36 +03:00
def sign_reason
'Signed with DocuSeal.co'
2023-09-10 19:25:57 +03:00
end
2023-10-11 21:05:42 +03:00
2023-12-01 18:42:22 +02:00
def add_logo(column, _submission = nil)
2023-10-11 21:05:42 +03:00
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
column.formatted_text([{ text: 'DocuSeal',
link: Docuseal::PRODUCT_URL }],
font_size: 20,
2024-05-02 13:40:01 +03:00
font: [FONT_NAME, { variant: :bold }],
2023-10-11 21:05:42 +03:00
width: 100,
2024-02-14 18:52:11 +02:00
padding: [5, 0, 0, 8],
2024-02-15 22:26:05 +02:00
position: :float, text_align: :left)
2023-10-11 21:05:42 +03:00
end
2023-09-10 19:25:57 +03:00
# rubocop:enable Metrics
end
end