PDF ai import (#1006)

Add support to review transactions for AI pdf import
This commit is contained in:
soky srm
2026-02-16 21:11:41 +01:00
committed by GitHub
parent 16aca7812a
commit d79d86d848
6 changed files with 26 additions and 18 deletions

View File

@@ -4,7 +4,10 @@ class Import::CleansController < ApplicationController
before_action :set_import before_action :set_import
def show def show
redirect_to import_configuration_path(@import), alert: "Please configure your import before proceeding." unless @import.configured? unless @import.configured?
redirect_path = @import.is_a?(PdfImport) ? import_path(@import) : import_configuration_path(@import)
return redirect_to redirect_path, alert: "Please configure your import before proceeding."
end
rows = @import.rows.ordered rows = @import.rows.ordered

View File

@@ -5,7 +5,7 @@ class ProcessPdfJob < ApplicationJob
return unless pdf_import.is_a?(PdfImport) return unless pdf_import.is_a?(PdfImport)
return unless pdf_import.pdf_uploaded? return unless pdf_import.pdf_uploaded?
return if pdf_import.status == "complete" return if pdf_import.status == "complete"
return if pdf_import.ai_processed? && (!pdf_import.bank_statement? || pdf_import.rows_count > 0) return if pdf_import.ai_processed? && (!pdf_import.statement_with_transactions? || pdf_import.rows_count > 0)
pdf_import.update!(status: :importing) pdf_import.update!(status: :importing)
@@ -14,9 +14,9 @@ class ProcessPdfJob < ApplicationJob
document_type = resolve_document_type(pdf_import, process_result) document_type = resolve_document_type(pdf_import, process_result)
upload_to_vector_store(pdf_import, document_type: document_type) upload_to_vector_store(pdf_import, document_type: document_type)
# For bank statements, extract transactions and generate import rows # For statements with transactions (bank/credit card), extract and generate import rows
if bank_statement_document?(document_type) if statement_with_transactions?(document_type)
Rails.logger.info("ProcessPdfJob: Extracting transactions for bank statement import #{pdf_import.id}") Rails.logger.info("ProcessPdfJob: Extracting transactions for #{document_type} import #{pdf_import.id}")
pdf_import.extract_transactions pdf_import.extract_transactions
Rails.logger.info("ProcessPdfJob: Extracted #{pdf_import.extracted_transactions.size} transactions") Rails.logger.info("ProcessPdfJob: Extracted #{pdf_import.extracted_transactions.size} transactions")
@@ -32,9 +32,9 @@ class ProcessPdfJob < ApplicationJob
pdf_import.send_next_steps_email(user) pdf_import.send_next_steps_email(user)
end end
# Bank statements with rows go to pending for user review/publish # Statements with extracted rows go to pending for user review/publish
# Non-bank statements are marked complete (no further action needed) # Other document types are marked complete (no further action needed)
final_status = bank_statement_document?(document_type) && pdf_import.rows_count > 0 ? :pending : :complete final_status = statement_with_transactions?(document_type) && pdf_import.rows_count > 0 ? :pending : :complete
pdf_import.update!(status: final_status) pdf_import.update!(status: final_status)
rescue StandardError => e rescue StandardError => e
sanitized_error = sanitize_error_message(e) sanitized_error = sanitize_error_message(e)
@@ -82,7 +82,7 @@ class ProcessPdfJob < ApplicationJob
pdf_import.reload.document_type pdf_import.reload.document_type
end end
def bank_statement_document?(document_type) def statement_with_transactions?(document_type)
document_type == "bank_statement" document_type.in?(%w[bank_statement credit_card_statement])
end end
end end

View File

@@ -68,7 +68,7 @@ class PdfImport < Import
end end
def extract_transactions def extract_transactions
return unless bank_statement? return unless statement_with_transactions?
provider = Provider::Registry.get_provider(:openai) provider = Provider::Registry.get_provider(:openai)
raise "AI provider not configured" unless provider raise "AI provider not configured" unless provider
@@ -91,6 +91,10 @@ class PdfImport < Import
document_type == "bank_statement" document_type == "bank_statement"
end end
def statement_with_transactions?
document_type.in?(%w[bank_statement credit_card_statement])
end
def has_extracted_transactions? def has_extracted_transactions?
extracted_data.present? && extracted_data["transactions"].present? extracted_data.present? && extracted_data["transactions"].present?
end end
@@ -147,7 +151,7 @@ class PdfImport < Import
end end
def publishable? def publishable?
account.present? && bank_statement? && cleaned? && mappings.all?(&:valid?) account.present? && statement_with_transactions? && cleaned? && mappings.all?(&:valid?)
end end
def column_keys def column_keys

View File

@@ -2,7 +2,7 @@
<%= render "imports/nav", import: @import %> <%= render "imports/nav", import: @import %>
<% end %> <% end %>
<%= content_for :previous_path, import_configuration_path(@import) %> <%= content_for :previous_path, @import.is_a?(PdfImport) ? import_path(@import) : import_configuration_path(@import) %>
<div class="space-y-4 mx-auto max-w-5xl"> <div class="space-y-4 mx-auto max-w-5xl">
<div class="text-center space-y-2 max-w-[400px] mx-auto mb-4"> <div class="text-center space-y-2 max-w-[400px] mx-auto mb-4">
@@ -20,7 +20,7 @@
<%= render DS::Link.new( <%= render DS::Link.new(
text: "Next step", text: "Next step",
variant: "primary", variant: "primary",
href: import_confirm_path(@import), href: @import.is_a?(PdfImport) ? import_path(@import) : import_confirm_path(@import),
frame: :_top, frame: :_top,
class: "w-full md:w-auto" class: "w-full md:w-auto"
) %> ) %>

View File

@@ -6,7 +6,7 @@
[ [
{ name: t("imports.steps.upload", default: "Upload"), path: nil, is_complete: import.pdf_uploaded?, step_number: 1 }, { name: t("imports.steps.upload", default: "Upload"), path: nil, is_complete: import.pdf_uploaded?, step_number: 1 },
{ name: t("imports.steps.configure", default: "Configure"), path: nil, is_complete: import.configured?, step_number: 2 }, { name: t("imports.steps.configure", default: "Configure"), path: nil, is_complete: import.configured?, step_number: 2 },
{ name: t("imports.steps.clean", default: "Clean"), path: nil, is_complete: import.cleaned?, step_number: 3 }, { name: t("imports.steps.clean", default: "Clean"), path: import.configured? ? import_clean_path(import) : nil, is_complete: import.cleaned?, step_number: 3 },
{ name: t("imports.steps.confirm", default: "Confirm"), path: import_path(import), is_complete: import.complete?, step_number: 4 } { name: t("imports.steps.confirm", default: "Confirm"), path: import_path(import), is_complete: import.complete?, step_number: 4 }
] ]
else else

View File

@@ -3,14 +3,14 @@
<div class="h-full flex flex-col justify-center items-center"> <div class="h-full flex flex-col justify-center items-center">
<div class="space-y-6 max-w-lg w-full"> <div class="space-y-6 max-w-lg w-full">
<% if import.pending? && import.rows_count > 0 %> <% if import.pending? && import.rows_count > 0 %>
<%# Bank statement with rows ready for review %> <%# Statement with rows ready for review %>
<div class="mx-auto bg-success/10 h-8 w-8 rounded-full flex items-center justify-center"> <div class="mx-auto bg-success/10 h-8 w-8 rounded-full flex items-center justify-center">
<%= icon "check", class: "text-success" %> <%= icon "check", class: "text-success" %>
</div> </div>
<div class="text-center space-y-2"> <div class="text-center space-y-2">
<h1 class="font-medium text-primary text-center text-3xl"><%= t("imports.pdf_import.ready_for_review_title", default: "Ready for Review") %></h1> <h1 class="font-medium text-primary text-center text-3xl"><%= t("imports.pdf_import.ready_for_review_title", default: "Ready for Review") %></h1>
<p class="text-sm text-secondary"><%= t("imports.pdf_import.ready_for_review_description", default: "We extracted %{count} transactions from your bank statement. Review and publish them to add to your account.", count: import.rows_count) %></p> <p class="text-sm text-secondary"><%= t("imports.pdf_import.ready_for_review_description", default: "We extracted %{count} transactions from your statement. Review and publish them to add to your account.", count: import.rows_count) %></p>
</div> </div>
<div class="bg-container border border-primary rounded-xl p-4 space-y-4"> <div class="bg-container border border-primary rounded-xl p-4 space-y-4">
@@ -53,8 +53,9 @@
<div class="space-y-2 flex flex-col"> <div class="space-y-2 flex flex-col">
<% if import.publishable? %> <% if import.publishable? %>
<%= button_to t("imports.pdf_import.publish_transactions", default: "Publish %{count} Transactions", count: import.rows_count), publish_import_path(import), method: :post, class: "w-full font-medium text-sm px-3 py-2 rounded-lg text-inverse bg-inverse hover:bg-inverse-hover" %> <%= button_to t("imports.pdf_import.publish_transactions", default: "Publish %{count} Transactions", count: import.rows_count), publish_import_path(import), method: :post, class: "w-full font-medium text-sm px-3 py-2 rounded-lg text-inverse bg-inverse hover:bg-inverse-hover" %>
<%= render DS::Link.new(text: t("imports.pdf_import.review_transactions", default: "Review Transactions"), href: import_clean_path(import), variant: "secondary", full_width: true) %>
<% elsif import.account.present? %> <% elsif import.account.present? %>
<%= render DS::Link.new(text: t("imports.pdf_import.review_transactions", default: "Review Transactions"), href: import_confirm_path(import), variant: "primary", full_width: true) %> <%= render DS::Link.new(text: t("imports.pdf_import.review_transactions", default: "Review Transactions"), href: import_clean_path(import), variant: "primary", full_width: true) %>
<% else %> <% else %>
<p class="text-center text-sm text-secondary"><%= t("imports.pdf_import.select_account_to_continue", default: "Please select an account above to continue.") %></p> <p class="text-center text-sm text-secondary"><%= t("imports.pdf_import.select_account_to_continue", default: "Please select an account above to continue.") %></p>
<% end %> <% end %>