diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index f1a217529..ef5f4b067 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -38,11 +38,17 @@ class ImportsController < ApplicationController def new @pending_import = Current.family.imports.ordered.pending.first + @document_upload_extensions = document_upload_supported_extensions end def create file = import_params[:import_file] + if file.present? && document_upload_request? + create_document_import(file) + return + end + # Handle PDF file uploads - process with AI if file.present? && Import::ALLOWED_PDF_MIME_TYPES.include?(file.content_type) unless valid_pdf_file?(file) @@ -137,6 +143,60 @@ class ImportsController < ApplicationController redirect_to import_path(pdf_import), notice: t("imports.create.pdf_processing") end + def create_document_import(file) + adapter = VectorStore.adapter + unless adapter + redirect_to new_import_path, alert: t("imports.create.document_provider_not_configured") + return + end + + if file.size > Import::MAX_PDF_SIZE + redirect_to new_import_path, alert: t("imports.create.document_too_large", max_size: Import::MAX_PDF_SIZE / 1.megabyte) + return + end + + filename = file.original_filename.to_s + ext = File.extname(filename).downcase + supported_extensions = adapter.supported_extensions.map(&:downcase) + + unless supported_extensions.include?(ext) + redirect_to new_import_path, alert: t("imports.create.invalid_document_file_type") + return + end + + if ext == ".pdf" + unless valid_pdf_file?(file) + redirect_to new_import_path, alert: t("imports.create.invalid_pdf") + return + end + + create_pdf_import(file) + return + end + + family_document = Current.family.upload_document( + file_content: file.read, + filename: filename + ) + + if family_document + redirect_to new_import_path, notice: t("imports.create.document_uploaded") + else + redirect_to new_import_path, alert: t("imports.create.document_upload_failed") + end + end + + def document_upload_supported_extensions + adapter = VectorStore.adapter + return [] unless adapter + + adapter.supported_extensions.map(&:downcase).uniq.sort + end + + def document_upload_request? + params.dig(:import, :type) == "DocumentImport" + end + def valid_pdf_file?(file) header = file.read(5) file.rewind diff --git a/app/views/imports/new.html.erb b/app/views/imports/new.html.erb index 91fc9f5ac..45ff29415 100644 --- a/app/views/imports/new.html.erb +++ b/app/views/imports/new.html.erb @@ -141,10 +141,10 @@ <% end %> - <% if (params[:type].nil? || params[:type] == "PdfImport") && Provider::Registry.get_provider(:openai)&.supports_pdf_processing? %> + <% if (params[:type].nil? || params[:type].in?(%w[DocumentImport PdfImport])) && @document_upload_extensions.any? %>
  • <%= styled_form_with url: imports_path, scope: :import, multipart: true, class: "w-full" do |form| %> - <%= form.hidden_field :type, value: "PdfImport" %> + <%= form.hidden_field :type, value: "DocumentImport" %> <% end %> diff --git a/config/locales/views/imports/en.yml b/config/locales/views/imports/en.yml index a40f3cbf0..740f9b2bd 100644 --- a/config/locales/views/imports/en.yml +++ b/config/locales/views/imports/en.yml @@ -102,11 +102,11 @@ en: import_portfolio: Import investments import_rules: Import rules import_transactions: Import transactions - import_pdf: Import PDF document - import_pdf_description: AI-powered document analysis + import_file: Import document + import_file_description: AI-powered analysis for PDFs and searchable upload for other supported files resume: Resume %{type} sources: Sources - title: New CSV Import + title: New Import create: file_too_large: File is too large. Maximum size is %{max_size}MB. invalid_file_type: Invalid file type. Please upload a CSV file. @@ -114,6 +114,11 @@ en: pdf_too_large: PDF file is too large. Maximum size is %{max_size}MB. pdf_processing: Your PDF is being processed. You will receive an email when analysis is complete. invalid_pdf: The uploaded file is not a valid PDF. + document_too_large: Document file is too large. Maximum size is %{max_size}MB. + invalid_document_file_type: Invalid document file type for the active vector store. + document_uploaded: Document uploaded successfully. + document_upload_failed: We couldn't upload the document to the vector store. Please try again. + document_provider_not_configured: No vector store is configured for document uploads. show: finalize_upload: Please finalize your file upload. finalize_mappings: Please finalize your mappings before proceeding. diff --git a/test/controllers/imports_controller_test.rb b/test/controllers/imports_controller_test.rb index b86684913..2b68173a7 100644 --- a/test/controllers/imports_controller_test.rb +++ b/test/controllers/imports_controller_test.rb @@ -35,6 +35,70 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to import_upload_url(Import.all.ordered.first) end + test "uploads supported non-pdf document for vector store without creating import" do + adapter = mock("vector_store_adapter") + adapter.stubs(:supported_extensions).returns(%w[.csv .pdf]) + VectorStore::Registry.stubs(:adapter).returns(adapter) + + @user.family.expects(:upload_document).with do |file_content:, filename:, **| + assert_not_empty file_content + assert_equal "valid.csv", filename + true + end.returns(family_documents(:tax_return)) + + assert_no_difference "Import.count" do + post imports_url, params: { + import: { + type: "DocumentImport", + import_file: file_fixture_upload("imports/valid.csv", "text/csv") + } + } + end + + assert_redirected_to new_import_url + assert_equal I18n.t("imports.create.document_uploaded"), flash[:notice] + end + + test "uploads pdf document as PdfImport when using DocumentImport option" do + adapter = mock("vector_store_adapter") + adapter.stubs(:supported_extensions).returns(%w[.pdf .txt]) + VectorStore::Registry.stubs(:adapter).returns(adapter) + + @user.family.expects(:upload_document).never + + assert_difference "Import.count", 1 do + post imports_url, params: { + import: { + type: "DocumentImport", + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + end + + created_import = Import.order(:created_at).last + assert_equal "PdfImport", created_import.type + assert_redirected_to import_url(created_import) + assert_equal I18n.t("imports.create.pdf_processing"), flash[:notice] + end + + test "rejects unsupported document type for DocumentImport option" do + adapter = mock("vector_store_adapter") + adapter.stubs(:supported_extensions).returns(%w[.pdf .txt]) + VectorStore::Registry.stubs(:adapter).returns(adapter) + + assert_no_difference "Import.count" do + post imports_url, params: { + import: { + type: "DocumentImport", + import_file: file_fixture_upload("profile_image.png", "image/png") + } + } + end + + assert_redirected_to new_import_url + assert_equal I18n.t("imports.create.invalid_document_file_type"), flash[:alert] + end + test "publishes import" do import = imports(:transaction)