diff --git a/app/jobs/process_pdf_job.rb b/app/jobs/process_pdf_job.rb index c577806dc..7d3878232 100644 --- a/app/jobs/process_pdf_job.rb +++ b/app/jobs/process_pdf_job.rb @@ -15,7 +15,8 @@ class ProcessPdfJob < ApplicationJob upload_to_vector_store(pdf_import, document_type: document_type) # For statements with transactions (bank/credit card), extract and generate import rows - if statement_with_transactions?(document_type) + # unless reconciliation already confirmed balances match + if statement_with_transactions?(document_type) && !pdf_import.reconciliation_matched? Rails.logger.info("ProcessPdfJob: Extracting transactions for #{document_type} import #{pdf_import.id}") pdf_import.extract_transactions Rails.logger.info("ProcessPdfJob: Extracted #{pdf_import.extracted_transactions.size} transactions") @@ -23,6 +24,8 @@ class ProcessPdfJob < ApplicationJob pdf_import.generate_rows_from_extracted_data pdf_import.sync_mappings Rails.logger.info("ProcessPdfJob: Generated #{pdf_import.rows_count} import rows") + elsif pdf_import.reconciliation_matched? + Rails.logger.info("ProcessPdfJob: Reconciliation matched for import #{pdf_import.id}, skipping transaction extraction") end # Find the user who created this import (first admin or any user in the family) @@ -32,9 +35,14 @@ class ProcessPdfJob < ApplicationJob pdf_import.send_next_steps_email(user) end - # Statements with extracted rows go to pending for user review/publish - # Other document types are marked complete (no further action needed) - final_status = statement_with_transactions?(document_type) && pdf_import.rows_count > 0 ? :pending : :complete + final_status = if pdf_import.reconciliation_matched? + :complete + elsif statement_with_transactions?(document_type) && pdf_import.rows_count > 0 + :pending + else + :complete + end + pdf_import.update!(status: final_status) rescue StandardError => e sanitized_error = sanitize_error_message(e) diff --git a/app/models/pdf_import.rb b/app/models/pdf_import.rb index 120c74c8e..b6de36d45 100644 --- a/app/models/pdf_import.rb +++ b/app/models/pdf_import.rb @@ -177,10 +177,16 @@ class PdfImport < Import end result = response.data - update!( + + attrs = { ai_summary: result.summary, document_type: result.document_type - ) + } + if result.reconciliation.present? + attrs[:extracted_data] = (result.extracted_data || {}).merge("reconciliation" => result.reconciliation) + end + + update!(attrs) result end @@ -215,6 +221,16 @@ class PdfImport < Import document_type.in?(%w[bank_statement credit_card_statement]) end + def reconciliation_data + extracted_data&.dig("reconciliation") + end + + def reconciliation_matched? + reconciliation_data.present? && + reconciliation_data["performed"] == true && + reconciliation_data["balance_match"] == true + end + def has_extracted_transactions? extracted_data.present? && extracted_data["transactions"].present? end diff --git a/app/models/provider/openai/pdf_processor.rb b/app/models/provider/openai/pdf_processor.rb index 99c768db9..60741cbea 100644 --- a/app/models/provider/openai/pdf_processor.rb +++ b/app/models/provider/openai/pdf_processor.rb @@ -97,6 +97,8 @@ class Provider::Openai::PdfProcessor "performed": true|false, "account_id": "account_id or null", "balance_match": true|false|null, + "statement_closing_balance": number|null, + "synced_closing_balance": number|null, "statement_transaction_count": number|null, "synced_transaction_count": number|null, "matched_count": number|null, diff --git a/app/views/imports/_nav.html.erb b/app/views/imports/_nav.html.erb index 0c9ee5aaa..2b8bda625 100644 --- a/app/views/imports/_nav.html.erb +++ b/app/views/imports/_nav.html.erb @@ -8,10 +8,11 @@ elsif import.is_a?(PdfImport) # PDF imports have a simplified flow: Upload -> Confirm # Upload/Configure/Clean are always complete for processed PDF imports + finalized = import.complete? [ { 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.clean", default: "Clean"), path: import.configured? ? import_clean_path(import) : nil, is_complete: import.cleaned?, step_number: 3 }, + { name: t("imports.steps.configure", default: "Configure"), path: nil, is_complete: finalized || import.configured?, step_number: 2 }, + { name: t("imports.steps.clean", default: "Clean"), path: (!finalized && import.configured?) ? import_clean_path(import) : nil, is_complete: finalized || import.cleaned?, step_number: 3 }, { name: t("imports.steps.confirm", default: "Confirm"), path: import_path(import), is_complete: import.complete?, step_number: 4 } ] elsif import.is_a?(QifImport) diff --git a/app/views/imports/_pdf_import.html.erb b/app/views/imports/_pdf_import.html.erb index 06f2a6e0e..5a0d2e40c 100644 --- a/app/views/imports/_pdf_import.html.erb +++ b/app/views/imports/_pdf_import.html.erb @@ -1,6 +1,6 @@ <%# locals: (import:) %> -
<%= t("imports.pdf_import.reconciliation_description") %>
++ <%= t("imports.document_types.#{import.document_type}", default: import.document_type&.humanize || t("imports.pdf_import.unknown_document_type", default: "Unknown")) %> +
+<%= import.ai_summary %>
+