diff --git a/app/controllers/export_controller.rb b/app/controllers/export_controller.rb
index cfa94cf4..bcde4d7e 100644
--- a/app/controllers/export_controller.rb
+++ b/app/controllers/export_controller.rb
@@ -32,6 +32,30 @@ class ExportController < ApplicationController
head :internal_server_error
end
+ def export_submission
+ submission = Submission.find(params[:id])
+ export_location = ExportLocation.default_location
+
+ unless export_location&.submissions_endpoint.present?
+ redirect_to submission, alert: 'Export failed: Submission export endpoint is not configured.'
+ return
+ end
+
+ payload = {
+ submission_id: submission.id,
+ template_name: submission.template&.name,
+ events: submission.submission_events.order(updated_at: :desc).limit(1)
+ }
+
+ response = post_to_api(payload, export_location.submissions_endpoint, export_location.extra_params)
+
+ if response&.success?
+ redirect_to submission, notice: "Submission ##{submission.id} events exported successfully."
+ else
+ redirect_to submission, alert: "Failed to export submission ##{submission.id} events."
+ end
+ end
+
private
def api_connection
diff --git a/app/models/document_generation_event.rb b/app/models/document_generation_event.rb
index 24d71ba3..f4e0a701 100644
--- a/app/models/document_generation_event.rb
+++ b/app/models/document_generation_event.rb
@@ -13,7 +13,7 @@
# Indexes
#
# index_document_generation_events_on_submitter_id (submitter_id)
-# index_document_generation_events_on_submitter_id_and_event_name (submitter_id,event_name) UNIQUE WHERE ((event_name)::text = ANY (ARRAY[('start'::character varying)::text, ('complete'::character varying)::text]))
+# index_document_generation_events_on_submitter_id_and_event_name (submitter_id,event_name) UNIQUE WHERE ((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))
#
# Foreign Keys
#
diff --git a/app/models/email_event.rb b/app/models/email_event.rb
index 3aaa5db9..3c6aac61 100644
--- a/app/models/email_event.rb
+++ b/app/models/email_event.rb
@@ -20,7 +20,7 @@
#
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
# index_email_events_on_email (email)
-# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY (ARRAY[('bounce'::character varying)::text, ('soft_bounce'::character varying)::text, ('complaint'::character varying)::text, ('soft_complaint'::character varying)::text]))
+# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
# index_email_events_on_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id)
#
diff --git a/app/models/export_location.rb b/app/models/export_location.rb
index a211c0a3..0be6dfb3 100644
--- a/app/models/export_location.rb
+++ b/app/models/export_location.rb
@@ -2,15 +2,16 @@
#
# Table name: export_locations
#
-# id :bigint not null, primary key
-# api_base_url :string not null
-# authorization_token :string
-# default_location :boolean default(FALSE), not null
-# extra_params :jsonb not null
-# name :string not null
-# templates_endpoint :string
-# created_at :datetime not null
-# updated_at :datetime not null
+# id :bigint not null, primary key
+# api_base_url :string not null
+# authorization_token :string
+# default_location :boolean default(FALSE), not null
+# extra_params :jsonb not null
+# name :string not null
+# submissions_endpoint :string
+# templates_endpoint :string
+# created_at :datetime not null
+# updated_at :datetime not null
#
class ExportLocation < ApplicationRecord
validates :name, presence: true
diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb
index cfa7f936..fc4804b1 100644
--- a/app/views/templates/_submission.html.erb
+++ b/app/views/templates/_submission.html.erb
@@ -101,6 +101,9 @@
<%= t('view') %>
+
+ <%= button_to button_title(title: nil, icon: svg_icon('upload', class: 'w-6 h-6')), export_submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('export'), method: :post %>
+
<% if !submission.archived_at? && !template&.archived_at? && can?(:destroy, submission) %>
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
diff --git a/config/routes.rb b/config/routes.rb
index 7fc306c2..18390c8a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -81,6 +81,8 @@ Rails.application.routes.draw do
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
resources :submissions, only: %i[show destroy] do
+ post :export, on: :member, to: 'export#export_submission'
+
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
resources :events, only: %i[index], controller: 'submission_events'
end
diff --git a/db/migrate/20250708172115_add_submissions_endpoint_to_export_locations.rb b/db/migrate/20250708172115_add_submissions_endpoint_to_export_locations.rb
new file mode 100644
index 00000000..2f3a31f8
--- /dev/null
+++ b/db/migrate/20250708172115_add_submissions_endpoint_to_export_locations.rb
@@ -0,0 +1,5 @@
+class AddSubmissionsEndpointToExportLocations < ActiveRecord::Migration[8.0]
+ def change
+ add_column :export_locations, :submissions_endpoint, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 9a97cf4f..326eb62d 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[8.0].define(version: 2025_07_03_143236) do
+ActiveRecord::Schema[8.0].define(version: 2025_07_08_172115) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql"
@@ -160,7 +160,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_143236) do
t.string "event_name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.index ["submitter_id", "event_name"], name: "index_document_generation_events_on_submitter_id_and_event_name", unique: true, where: "((event_name)::text = ANY (ARRAY[('start'::character varying)::text, ('complete'::character varying)::text]))"
+ t.index ["submitter_id", "event_name"], name: "index_document_generation_events_on_submitter_id_and_event_name", unique: true, where: "((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))"
t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id"
end
@@ -177,7 +177,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_143236) do
t.datetime "created_at", null: false
t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime"
t.index ["email"], name: "index_email_events_on_email"
- t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY (ARRAY[('bounce'::character varying)::text, ('soft_bounce'::character varying)::text, ('complaint'::character varying)::text, ('soft_complaint'::character varying)::text]))"
+ t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))"
t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable"
t.index ["message_id"], name: "index_email_events_on_message_id"
end
@@ -225,6 +225,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_143236) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.jsonb "extra_params", default: {}, null: false
+ t.string "submissions_endpoint"
end
create_table "oauth_access_grants", force: :cascade do |t|