diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 454ac39b..daeb44cc 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -50,7 +50,7 @@ class AccountsController < ApplicationController # rubocop:disable Layout/LineLength render turbo_stream: turbo_stream.replace( :account_delete_button, - html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account')) + html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) ) # rubocop:enable Layout/LineLength end diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 209c81b4..4512e018 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -64,6 +64,7 @@ class StartFormController < ApplicationController .or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .order(id: :desc) .where(declined_at: nil) + .where(external_id: nil) .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .find_or_initialize_by(**submitter_params.compact_blank) end diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb index 0d61cb00..862b45d9 100644 --- a/app/controllers/submissions_preview_controller.rb +++ b/app/controllers/submissions_preview_controller.rb @@ -20,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController @submission ||= Submission.find_by!(slug: params[:slug]) - if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?) + raise ActionController::RoutingError if @submission.account.archived_at? + + if !@submission.submitters.all?(&:completed_at?) && !signature_valid && + (!current_user || !current_ability.can?(:read, @submission)) raise ActionController::RoutingError, I18n.t('not_found') end diff --git a/app/controllers/submitters_resubmit_controller.rb b/app/controllers/submitters_resubmit_controller.rb new file mode 100644 index 00000000..fb893d98 --- /dev/null +++ b/app/controllers/submitters_resubmit_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class SubmittersResubmitController < ApplicationController + load_and_authorize_resource :submitter, parent: false + + def update + return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email + + submission = @submitter.template.submissions.new(created_by_user: current_user, + submitters_order: :preserved, + **@submitter.submission.slice(:template_fields, + :account_id, + :template_schema, + :template_submitters, + :preferences)) + + @submitter.submission.submitters.each do |submitter| + new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name, + :preferences, :metadata, :account_id)) + + next unless submitter.uuid == @submitter.uuid + + assign_submitter_values(new_submitter, submitter) + + @new_submitter ||= new_submitter + end + + submission.save! + + redirect_to submit_form_path(slug: @new_submitter.slug) + end + + private + + def assign_submitter_values(new_submitter, submitter) + attachments_index = submitter.attachments.index_by(&:uuid) + + submitter.submission.template_fields.each do |field| + next if field['submitter_uuid'] != submitter.uuid + next if field['default_value'] == '{{date}}' + next if field['type'] == 'stamp' + next if field['type'] == 'signature' + next if field.dig('preferences', 'formula').present? + + value = submitter.values[field['uuid']] + + next if value.blank? + + if field['type'].in?(%w[image file initials]) + Array.wrap(value).each do |attachment_uuid| + new_submitter.attachments << attachments_index[attachment_uuid].dup + end + end + + new_submitter.values[field['uuid']] = value + end + end +end diff --git a/app/javascript/submission_form/dropzone.vue b/app/javascript/submission_form/dropzone.vue index 19285c4e..fafcd4ed 100644 --- a/app/javascript/submission_form/dropzone.vue +++ b/app/javascript/submission_form/dropzone.vue @@ -128,7 +128,7 @@ export default { } }) } else { - if (file.type === 'image/bmp') { + if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') { file = await this.convertBmpToPng(file) } diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index ccb62ab9..1ada02dc 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -361,6 +361,7 @@ export default { isMoved: false, renderDropdown: false, isNameFocus: false, + isHeadingSelected: false, textOverflowChars: 0, dragFrom: { x: 0, y: 0 } } @@ -377,7 +378,7 @@ export default { } }, isValueInput () { - return (this.field.type === 'heading' && this.isSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type)) + return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type)) }, modalContainerEl () { return this.$el.getRootNode().querySelector('#docuseal_modal_container') @@ -485,7 +486,7 @@ export default { if (['text', 'number'].includes(this.field.type)) { this.isContenteditable = true - this.$nextTick(() => this.focusValueInput()) + this.focusValueInput() } else if (this.field.type === 'checkbox') { this.field.readonly = !this.field.readonly this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true @@ -507,16 +508,18 @@ export default { } }, focusValueInput (e) { - if (this.$refs.defaultValue !== document.activeElement) { - this.$refs.defaultValue.focus() - - if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) { - window.getSelection().collapse( - this.$refs.defaultValue.firstChild, - this.$refs.defaultValue.innerText.length - ) + this.$nextTick(() => { + if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) { + this.$refs.defaultValue.focus() + + if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) { + window.getSelection().collapse( + this.$refs.defaultValue.firstChild, + this.$refs.defaultValue.innerText.length + ) + } } - } + }) }, formatNumber (number, format) { if (format === 'comma') { @@ -632,6 +635,7 @@ export default { const text = this.$refs.defaultValue.innerText.trim() this.isContenteditable = false + this.isHeadingSelected = false if (text) { if (this.field.type === 'number') { @@ -749,10 +753,6 @@ export default { this.selectedAreaRef.value = this.area - if (this.field.type === 'heading') { - this.$nextTick(() => this.focusValueInput()) - } - this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY } this.$el.getRootNode().addEventListener('mousemove', this.mouseMove) @@ -787,6 +787,12 @@ export default { this.save() } + if (this.field.type === 'heading') { + this.isHeadingSelected = !this.isMoved + + this.focusValueInput() + } + this.isDragged = false this.isMoved = false diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 70e9b0f6..6e8bae2a 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -1427,6 +1427,8 @@ export default { const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value) + areaRef.isHeadingSelected = true + areaRef.focusValueInput() }) } diff --git a/app/javascript/template_builder/field_submitter.vue b/app/javascript/template_builder/field_submitter.vue index b5949e55..e1015835 100644 --- a/app/javascript/template_builder/field_submitter.vue +++ b/app/javascript/template_builder/field_submitter.vue @@ -143,6 +143,7 @@
  • -
  • +
  • f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase())) + return this.submitterDefaultFields.filter((f) => (f.title || f.name).toLowerCase().includes(this.defaultFieldsSearch.toLowerCase())) } else { return this.submitterDefaultFields } diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index d8fa6f2e..73a6c17f 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -1,4 +1,5 @@ const en = { + party: 'Party', method: 'Method', reorder_fields: 'Reorder fields', verify_id: 'Verify ID', @@ -160,6 +161,7 @@ const en = { } const es = { + party: 'Parte', method: 'Método', reorder_fields: 'Reordenar campos', verify_id: 'Verificar ID', @@ -321,6 +323,7 @@ const es = { } const it = { + party: 'Parte', method: 'Metodo', reorder_fields: 'Riordina i campi', verify_id: 'Verifica ID', @@ -482,6 +485,7 @@ const it = { } const pt = { + party: 'Parte', method: 'Método', reorder_fields: 'Reorganizar campos', verify_id: 'Verificar ID', @@ -643,6 +647,7 @@ const pt = { } const fr = { + party: 'Partie', method: 'Méthode', reorder_fields: 'Réorganiser les champs', verify_id: "Vérifier l'ID", @@ -804,6 +809,7 @@ const fr = { } const de = { + party: 'Partei', method: 'Verfahren', reorder_fields: 'Felder neu anordnen', verify_id: 'ID überprüfen', diff --git a/app/jobs/send_form_completed_webhook_request_job.rb b/app/jobs/send_form_completed_webhook_request_job.rb index 253ebf33..48951724 100644 --- a/app/jobs/send_form_completed_webhook_request_job.rb +++ b/app/jobs/send_form_completed_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendFormCompletedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 20 def perform(params = {}) @@ -21,19 +19,8 @@ class SendFormCompletedWebhookRequestJob ActiveStorage::Current.url_options = Docuseal.default_url_options - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'form.completed', - timestamp: Time.current, - data: Submitters::SerializeForWebhook.call(submitter) - }.to_json, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT, - **webhook_url.secret.to_h) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed', + data: Submitters::SerializeForWebhook.call(submitter)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_form_declined_webhook_request_job.rb b/app/jobs/send_form_declined_webhook_request_job.rb index 86fdae2a..1c7a1e32 100644 --- a/app/jobs/send_form_declined_webhook_request_job.rb +++ b/app/jobs/send_form_declined_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendFormDeclinedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -19,19 +17,8 @@ class SendFormDeclinedWebhookRequestJob ActiveStorage::Current.url_options = Docuseal.default_url_options - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'form.declined', - timestamp: Time.current, - data: Submitters::SerializeForWebhook.call(submitter) - }.to_json, - **webhook_url.secret.to_h, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'form.declined', + data: Submitters::SerializeForWebhook.call(submitter)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_form_started_webhook_request_job.rb b/app/jobs/send_form_started_webhook_request_job.rb index 3a6e0eae..44510f46 100644 --- a/app/jobs/send_form_started_webhook_request_job.rb +++ b/app/jobs/send_form_started_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendFormStartedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -19,19 +17,8 @@ class SendFormStartedWebhookRequestJob ActiveStorage::Current.url_options = Docuseal.default_url_options - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'form.started', - timestamp: Time.current, - data: Submitters::SerializeForWebhook.call(submitter) - }.to_json, - **webhook_url.secret.to_h, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'form.started', + data: Submitters::SerializeForWebhook.call(submitter)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_form_viewed_webhook_request_job.rb b/app/jobs/send_form_viewed_webhook_request_job.rb index c70cf776..162743e4 100644 --- a/app/jobs/send_form_viewed_webhook_request_job.rb +++ b/app/jobs/send_form_viewed_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendFormViewedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -19,19 +17,8 @@ class SendFormViewedWebhookRequestJob ActiveStorage::Current.url_options = Docuseal.default_url_options - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'form.viewed', - timestamp: Time.current, - data: Submitters::SerializeForWebhook.call(submitter) - }.to_json, - **webhook_url.secret.to_h, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'form.viewed', + data: Submitters::SerializeForWebhook.call(submitter)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_submission_archived_webhook_request_job.rb b/app/jobs/send_submission_archived_webhook_request_job.rb index 334d047b..82fc271f 100644 --- a/app/jobs/send_submission_archived_webhook_request_job.rb +++ b/app/jobs/send_submission_archived_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendSubmissionArchivedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -17,19 +15,8 @@ class SendSubmissionArchivedWebhookRequestJob return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived') - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'submission.archived', - timestamp: Time.current, - data: submission.as_json(only: %i[id archived_at]) - }.to_json, - **webhook_url.secret.to_h, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.archived', + data: submission.as_json(only: %i[id archived_at])) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_submission_completed_webhook_request_job.rb b/app/jobs/send_submission_completed_webhook_request_job.rb index d6037d0e..375bfa75 100644 --- a/app/jobs/send_submission_completed_webhook_request_job.rb +++ b/app/jobs/send_submission_completed_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendSubmissionCompletedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -17,19 +15,8 @@ class SendSubmissionCompletedWebhookRequestJob return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed') - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'submission.completed', - timestamp: Time.current, - data: Submissions::SerializeForApi.call(submission) - }.to_json, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT, - **webhook_url.secret.to_h) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.completed', + data: Submissions::SerializeForApi.call(submission)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_submission_created_webhook_request_job.rb b/app/jobs/send_submission_created_webhook_request_job.rb index a4dba4a6..d798e76a 100644 --- a/app/jobs/send_submission_created_webhook_request_job.rb +++ b/app/jobs/send_submission_created_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendSubmissionCreatedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -17,19 +15,8 @@ class SendSubmissionCreatedWebhookRequestJob return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created') - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'submission.created', - timestamp: Time.current, - data: Submissions::SerializeForApi.call(submission) - }.to_json, - **webhook_url.secret.to_h, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.created', + data: Submissions::SerializeForApi.call(submission)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_template_created_webhook_request_job.rb b/app/jobs/send_template_created_webhook_request_job.rb index 6b2493dc..353ecb6d 100644 --- a/app/jobs/send_template_created_webhook_request_job.rb +++ b/app/jobs/send_template_created_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendTemplateCreatedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -17,19 +15,8 @@ class SendTemplateCreatedWebhookRequestJob return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created') - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'template.created', - timestamp: Time.current, - data: Templates::SerializeForApi.call(template) - }.to_json, - **webhook_url.secret.to_h, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'template.created', + data: Templates::SerializeForApi.call(template)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) diff --git a/app/jobs/send_template_updated_webhook_request_job.rb b/app/jobs/send_template_updated_webhook_request_job.rb index 68479f1b..30623e15 100644 --- a/app/jobs/send_template_updated_webhook_request_job.rb +++ b/app/jobs/send_template_updated_webhook_request_job.rb @@ -5,8 +5,6 @@ class SendTemplateUpdatedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.com Webhook' - MAX_ATTEMPTS = 10 def perform(params = {}) @@ -17,19 +15,8 @@ class SendTemplateUpdatedWebhookRequestJob return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated') - resp = begin - Faraday.post(webhook_url.url, - { - event_type: 'template.updated', - timestamp: Time.current, - data: Templates::SerializeForApi.call(template) - }.to_json, - **webhook_url.secret.to_h, - 'Content-Type' => 'application/json', - 'User-Agent' => USER_AGENT) - rescue Faraday::Error - nil - end + resp = SendWebhookRequest.call(webhook_url, event_type: 'template.updated', + data: Templates::SerializeForApi.call(template)) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index bdb4a26b..0a65fd2e 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -1,3 +1,4 @@ +<%= render 'shared/navbar_warning' %>
    diff --git a/app/views/shared/_navbar_warning.html.erb b/app/views/shared/_navbar_warning.html.erb new file mode 100644 index 00000000..e69de29b diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 083651cd..09d57646 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -118,7 +118,7 @@
    <% end %> + <% if signed_in? && submitter && submitter.completed_at? && submitter.email == current_user.email && submitter.completed_at > 1.month.ago && can?(:update, @submission) %> +
    + <%= button_to t('resubmit'), submitters_resubmit_path(submitter), method: :put, class: 'btn btn-sm btn-primary w-full', form: { target: '_blank' }, data: { turbo: false } %> +
    + <% end %>
    diff --git a/app/views/submit_form/completed.html.erb b/app/views/submit_form/completed.html.erb index 85c5a129..92114274 100644 --- a/app/views/submit_form/completed.html.erb +++ b/app/views/submit_form/completed.html.erb @@ -42,7 +42,8 @@ <% end %>
    - <% if Templates.filter_undefined_submitters(@submitter.submission.template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %> + <% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template) %> + <% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %>
    <%= t('or') %>
    <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 83a9d9e0..e710474a 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -165,7 +165,7 @@ en: &en schedule_account_for_deletion_: Schedule account for deletion? account_information_has_been_updated: Account information has been updated. should_be_a_valid_url: should be a valid URL - your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 weeks. Please contact us if you want to keep your account. + your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 months. Please contact us if you want to keep your account. test_mode: Test mode copy: Copy copied: Copied @@ -688,6 +688,8 @@ en: &en tell_us_more_about_your_experience: Tell us more about your experience extremely_dissatisfied: Extremely Dissatisfied extremely_satisfied: Extremely Satisfied + your_pro_plan_payment_is_overdue: Your Pro plan payment is overdue. + click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: Click here to update your payment details and clear the invoice to ensure uninterrupted service. submission_event_names: send_email_to_html: 'Email sent to %{submitter_name}' send_reminder_email_to_html: 'Reminder email sent to %{submitter_name}' @@ -873,7 +875,7 @@ es: &es schedule_account_for_deletion_: ¿Programar la eliminación de la cuenta? account_information_has_been_updated: La información de la cuenta ha sido actualizada. should_be_a_valid_url: debe ser una URL válida - your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 semanas. Por favor contáctanos si deseas mantener tu cuenta. + your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 meses. Por favor contáctanos si deseas mantener tu cuenta. test_mode: Modo de prueba copy: Copiar copied: Copiado @@ -1396,6 +1398,8 @@ es: &es tell_us_more_about_your_experience: Cuéntanos más sobre tu experiencia extremely_dissatisfied: Extremadamente insatisfecho extremely_satisfied: Extremadamente satisfecho + your_pro_plan_payment_is_overdue: El pago de tu plan Pro está atrasado. + click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Haz clic aquí para actualizar tus datos de pago y liquidar la factura para garantizar un servicio ininterrumpido.' submission_event_names: send_email_to_html: 'Correo electrónico enviado a %{submitter_name}' send_reminder_email_to_html: 'Correo de recordatorio enviado a %{submitter_name}' @@ -1580,7 +1584,7 @@ it: &it schedule_account_for_deletion_: "Programmare l'eliminazione dell'account?" account_information_has_been_updated: "Le informazioni dell'account sono state aggiornate." should_be_a_valid_url: deve essere un URL valido - your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 settimane. Contattaci se desideri mantenere il tuo account." + your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 mesi. Contattaci se desideri mantenere il tuo account." test_mode: Modalità di test copy: Copia copied: Copiato @@ -2103,6 +2107,8 @@ it: &it tell_us_more_about_your_experience: Raccontaci di più sulla tua esperienza extremely_dissatisfied: Estremamente insoddisfatto extremely_satisfied: Estremamente soddisfatto + your_pro_plan_payment_is_overdue: Il pagamento del tuo piano Pro è in ritardo. + click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Fai clic qui per aggiornare i tuoi dati di pagamento e saldare la fattura per garantire un servizio ininterrotto.' submission_event_names: send_email_to_html: 'E-mail inviato a %{submitter_name}' send_reminder_email_to_html: 'E-mail di promemoria inviato a %{submitter_name}' @@ -2289,7 +2295,7 @@ fr: &fr schedule_account_for_deletion_: Programmer la suppression du compte? account_information_has_been_updated: Les informations du compte ont été mises à jour. should_be_a_valid_url: doit être une URL valide - your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression du compte sera traitée dans un délai de 2 semaines. Veuillez nous contacter si vous souhaitez conserver votre compte. + your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression du compte sera traitée dans un délai de 2 mois. Veuillez nous contacter si vous souhaitez conserver votre compte. test_mode: Mode test copy: Copier copied: Copié @@ -2812,6 +2818,8 @@ fr: &fr tell_us_more_about_your_experience: Parlez-nous davantage de votre expérience extremely_dissatisfied: Extrêmement insatisfait extremely_satisfied: Extrêmement satisfait + your_pro_plan_payment_is_overdue: Le paiement de votre plan Pro est en retard. + click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Cliquez ici pour mettre à jour vos informations de paiement et régler la facture afin de garantir un service ininterrompu.' submission_event_names: send_email_to_html: 'E-mail envoyé à %{submitter_name}' send_reminder_email_to_html: 'E-mail de rappel envoyé à %{submitter_name}' @@ -2997,7 +3005,7 @@ pt: &pt schedule_account_for_deletion_: Agendar exclusão da conta? account_information_has_been_updated: As informações da conta foram atualizadas. should_be_a_valid_url: deve ser um URL válido - your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 semanas. Entre em contato conosco se você quiser manter sua conta. + your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 meses. Entre em contato conosco se você quiser manter sua conta. test_mode: Modo de teste copy: Copiar copied: Copiado @@ -3520,6 +3528,8 @@ pt: &pt tell_us_more_about_your_experience: Conte-nos mais sobre sua experiência extremely_dissatisfied: Extremamente insatisfeito extremely_satisfied: Extremamente satisfeito + your_pro_plan_payment_is_overdue: O pagamento do seu plano Pro está atrasado. + click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Clique aqui para atualizar seus dados de pagamento e quitar a fatura para garantir um serviço ininterrupto.' submission_event_names: send_email_to_html: 'E-mail enviado para %{submitter_name}' send_reminder_email_to_html: 'E-mail de lembrete enviado para %{submitter_name}' @@ -3705,7 +3715,7 @@ de: &de schedule_account_for_deletion_: Konto zur Löschung einplanen? account_information_has_been_updated: Die Kontoinformationen wurden aktualisiert. should_be_a_valid_url: sollte eine gültige URL sein - your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Deine Anfrage zur Kontolöschung wird innerhalb von 2 Wochen bearbeitet. Bitte kontaktiere uns, wenn du dein Konto behalten möchtest. + your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Deine Anfrage zur Kontolöschung wird innerhalb von 2 Monaten bearbeitet. Bitte kontaktiere uns, wenn du dein Konto behalten möchtest. test_mode: Testmodus copy: Kopieren copied: Kopiert @@ -4228,6 +4238,8 @@ de: &de tell_us_more_about_your_experience: Erzählen Sie uns mehr über Ihre Erfahrung extremely_dissatisfied: Extrem unzufrieden extremely_satisfied: Extrem zufrieden + your_pro_plan_payment_is_overdue: Ihre Zahlung für den Pro-Plan ist überfällig. + click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: 'Klicken Sie hier, um Ihre Zahlungsdaten zu aktualisieren und die Rechnung zu begleichen, um einen unterbrechungsfreien Service sicherzustellen.' submission_event_names: send_email_to_html: 'E-Mail gesendet an %{submitter_name}' send_reminder_email_to_html: 'Erinnerungs-E-Mail gesendet an %{submitter_name}' diff --git a/config/routes.rb b/config/routes.rb index dd568316..b2f84549 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,6 +79,7 @@ Rails.application.routes.draw do resource :testing_account, only: %i[show destroy] resources :testing_api_settings, only: %i[index] resources :submitters_autocomplete, only: %i[index] + resources :submitters_resubmit, only: %i[update] resources :template_folders_autocomplete, only: %i[index] resources :webhook_secret, only: %i[show update] resources :webhook_preferences, only: %i[update] diff --git a/lib/send_webhook_request.rb b/lib/send_webhook_request.rb new file mode 100644 index 00000000..d2e6ebf4 --- /dev/null +++ b/lib/send_webhook_request.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module SendWebhookRequest + USER_AGENT = 'DocuSeal.com Webhook' + + module_function + + def call(webhook_url, event_type:, data:) + Faraday.post(webhook_url.url) do |req| + req.headers['Content-Type'] = 'application/json' + req.headers['User-Agent'] = USER_AGENT + req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present? + + req.body = { + event_type: event_type, + timestamp: Time.current, + data: data + }.to_json + + req.options.read_timeout = 8 + req.options.open_timeout = 8 + end + rescue Faraday::Error + nil + end +end diff --git a/lib/submissions.rb b/lib/submissions.rb index 86988d09..fc1f6810 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -107,20 +107,27 @@ module Submissions def normalize_email(email) return if email.blank? - return email.downcase if email.to_s.include?(',') - return email.downcase if email.to_s.include?('.gob') - return email.downcase if email.to_s.include?('.om') - return email.downcase if email.to_s.include?('.mm') - return email.downcase if email.to_s.include?('.cm') - return email.downcase if email.to_s.include?('.et') - return email.downcase if email.to_s.include?('.mo') - return email.downcase if email.to_s.include?('.nz') - return email.downcase if email.to_s.include?('.za') - return email.downcase unless email.to_s.include?('.') + + return email.downcase if email.to_s.include?(',') || + email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) || + email.to_s.exclude?('.') fixed_email = EmailTypo.call(email.delete_prefix('<')) - Rails.logger.info("Fixed email #{email.split('@').last}") if fixed_email != email.downcase.delete_prefix('<').strip + return fixed_email if fixed_email == email + + domain = email.to_s.split('@').last.to_s.downcase + fixed_domain = fixed_email.to_s.split('@').last + + return email.downcase if domain == fixed_domain + + if DidYouMean::Levenshtein.distance(domain, fixed_domain) > 3 + Rails.logger.info("Skipped email fix #{domain}") + + return email.downcase + end + + Rails.logger.info("Fixed email #{domain}") if fixed_email != email.downcase.delete_prefix('<').strip fixed_email end