diff --git a/app/jobs/process_pdf_job.rb b/app/jobs/process_pdf_job.rb index b30e8db09..7cfe906b5 100644 --- a/app/jobs/process_pdf_job.rb +++ b/app/jobs/process_pdf_job.rb @@ -14,9 +14,15 @@ class ProcessPdfJob < ApplicationJob document_type = resolve_document_type(pdf_import, process_result) upload_to_vector_store(pdf_import, document_type: document_type) - # For statements with transactions (bank/credit card), extract and generate import rows - # unless reconciliation already confirmed balances match - if statement_with_transactions?(document_type) && !pdf_import.reconciliation_matched? + if pdf_import.reconciliation_matched? + Rails.logger.info("ProcessPdfJob: Reconciliation matched for import #{pdf_import.id}, skipping transaction extraction") + elsif statement_with_transactions?(document_type) + if pdf_import.reconciliation_reportable? && pdf_import.account.nil? + recon_account = pdf_import.reconciliation_account + pdf_import.update!(account: recon_account) if recon_account + Rails.logger.info("ProcessPdfJob: Auto-assigned account #{recon_account&.name} from reconciliation for import #{pdf_import.id}") + end + 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") @@ -24,8 +30,6 @@ 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) diff --git a/app/models/pdf_import.rb b/app/models/pdf_import.rb index 1b16e92d9..7e28f0e97 100644 --- a/app/models/pdf_import.rb +++ b/app/models/pdf_import.rb @@ -107,10 +107,22 @@ class PdfImport < Import extracted_data&.dig("reconciliation") end + def reconciliation_reportable? + recon = reconciliation_data + return false unless recon.present? + return false unless recon["performed"] == true + return false unless recon["account_id"].present? + return false if recon["statement_transaction_count"].to_i == 0 + true + end + def reconciliation_matched? - reconciliation_data.present? && - reconciliation_data["performed"] == true && - reconciliation_data["balance_match"] == true + reconciliation_reportable? && reconciliation_data["balance_match"] == true + end + + def reconciliation_account + return nil unless reconciliation_data&.dig("account_id").present? + family.accounts.find_by(id: reconciliation_data["account_id"]) end def has_extracted_transactions? diff --git a/app/views/imports/_nav.html.erb b/app/views/imports/_nav.html.erb index 2b8bda625..7ab89f692 100644 --- a/app/views/imports/_nav.html.erb +++ b/app/views/imports/_nav.html.erb @@ -8,7 +8,7 @@ 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? + finalized = import.complete? || (import.respond_to?(:reconciliation_reportable?) && import.reconciliation_reportable?) [ { 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: finalized || import.configured?, step_number: 2 }, diff --git a/app/views/imports/_pdf_import.html.erb b/app/views/imports/_pdf_import.html.erb index a3f1e48f5..ffea59522 100644 --- a/app/views/imports/_pdf_import.html.erb +++ b/app/views/imports/_pdf_import.html.erb @@ -2,8 +2,92 @@
- <% if import.pending? && import.rows_count > 0 %> - <%# Statement with rows ready for review %> + <% if import.respond_to?(:reconciliation_reportable?) && import.reconciliation_reportable? && (import.complete? || (import.pending? && import.rows_count > 0)) %> + <% recon = import.reconciliation_data %> + <% balance_matched = import.reconciliation_matched? %> + +
"> + <%= icon balance_matched ? "check" : "alert-triangle", class: balance_matched ? "text-success" : "text-yellow-600" %> +
+ +
+

<%= t("imports.pdf_import.reconciliation_title") %>

+

<%= balance_matched ? t("imports.pdf_import.reconciliation_description") : t("imports.pdf_import.reconciliation_mismatch_description") %>

+
+ +
+
+

<%= t("imports.pdf_import.document_type_label") %>

+

+ <%= t("imports.document_types.#{import.document_type}", default: import.document_type&.humanize || t("imports.pdf_import.unknown_document_type", default: "Unknown")) %> +

+
+ +
+

<%= t("imports.pdf_import.reconciliation_details_label") %>

+
+ <% if recon["statement_closing_balance"].present? %> +
+ <%= t("imports.pdf_import.reconciliation_statement_balance") %> + <%= number_to_currency(recon["statement_closing_balance"], unit: import.extracted_data&.dig("currency") || "") %> +
+ <% end %> + <% if recon["synced_closing_balance"].present? %> +
+ <%= t("imports.pdf_import.reconciliation_synced_balance") %> + <%= number_to_currency(recon["synced_closing_balance"], unit: import.extracted_data&.dig("currency") || "") %> +
+ <% end %> +
+ <%= t("imports.pdf_import.reconciliation_balance_match") %> + <% if balance_matched %> + <%= t("imports.pdf_import.reconciliation_balance_yes") %> + <% else %> + <%= t("imports.pdf_import.reconciliation_balance_no") %> + <% end %> +
+ <% if recon["statement_transaction_count"].present? %> +
+ <%= t("imports.pdf_import.reconciliation_statement_txns") %> + <%= recon["statement_transaction_count"] %> +
+ <% end %> + <% if recon["synced_transaction_count"].present? %> +
+ <%= t("imports.pdf_import.reconciliation_synced_txns") %> + <%= recon["synced_transaction_count"] %> +
+ <% end %> + <% if recon["matched_count"].present? %> +
+ <%= t("imports.pdf_import.reconciliation_matched_txns") %> + <%= recon["matched_count"] %> +
+ <% end %> +
+
+ +
+

<%= t("imports.pdf_import.summary_label") %>

+

<%= import.ai_summary %>

+
+
+ +
+ <% if !balance_matched && import.pending? && import.rows_count > 0 %> + <% 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" %> + <%= 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? %> + <%= render DS::Link.new(text: t("imports.pdf_import.review_transactions", default: "Review Transactions"), href: import_clean_path(import), variant: "primary", full_width: true) %> + <% end %> + <% end %> + <%= render DS::Link.new(text: t("imports.pdf_import.back_to_imports"), href: imports_path, variant: balance_matched ? "primary" : "secondary", full_width: true) %> + <%= button_to t("imports.pdf_import.delete_import"), import_path(import), method: :delete, class: "w-full font-medium text-sm px-3 py-2 rounded-lg text-primary bg-gray-200 hover:bg-gray-300" %> +
+ + <% elsif import.pending? && import.rows_count > 0 %> + <%# Statement with rows ready for review (no reconciliation) %>
<%= icon "check", class: "text-success" %>
@@ -95,77 +179,6 @@ <%= button_to t("imports.pdf_import.delete_import"), import_path(import), method: :delete, class: "w-full font-medium text-sm px-3 py-2 rounded-lg text-primary bg-gray-200 hover:bg-gray-300" %>
- <% elsif import.complete? && import.respond_to?(:reconciliation_matched?) && import.reconciliation_matched? %> - <% recon = import.reconciliation_data %> - -
- <%= icon "check", class: "text-success" %> -
- -
-

<%= t("imports.pdf_import.reconciliation_title") %>

-

<%= t("imports.pdf_import.reconciliation_description") %>

-
- -
-
-

<%= t("imports.pdf_import.document_type_label") %>

-

- <%= t("imports.document_types.#{import.document_type}", default: import.document_type&.humanize || t("imports.pdf_import.unknown_document_type", default: "Unknown")) %> -

-
- -
-

<%= t("imports.pdf_import.reconciliation_details_label") %>

-
- <% if recon["statement_closing_balance"].present? %> -
- <%= t("imports.pdf_import.reconciliation_statement_balance") %> - <%= number_to_currency(recon["statement_closing_balance"], unit: import.extracted_data&.dig("currency") || "") %> -
- <% end %> - <% if recon["synced_closing_balance"].present? %> -
- <%= t("imports.pdf_import.reconciliation_synced_balance") %> - <%= number_to_currency(recon["synced_closing_balance"], unit: import.extracted_data&.dig("currency") || "") %> -
- <% end %> -
- <%= t("imports.pdf_import.reconciliation_balance_match") %> - <%= t("imports.pdf_import.reconciliation_balance_yes") %> -
- <% if recon["statement_transaction_count"].present? %> -
- <%= t("imports.pdf_import.reconciliation_statement_txns") %> - <%= recon["statement_transaction_count"] %> -
- <% end %> - <% if recon["synced_transaction_count"].present? %> -
- <%= t("imports.pdf_import.reconciliation_synced_txns") %> - <%= recon["synced_transaction_count"] %> -
- <% end %> - <% if recon["matched_count"].present? %> -
- <%= t("imports.pdf_import.reconciliation_matched_txns") %> - <%= recon["matched_count"] %> -
- <% end %> -
-
- -
-

<%= t("imports.pdf_import.summary_label") %>

-

<%= import.ai_summary %>

-
-
- -
- <%= render DS::Link.new(text: t("imports.pdf_import.back_to_imports"), href: imports_path, variant: "primary", full_width: true) %> - <%= button_to t("imports.pdf_import.delete_import"), import_path(import), method: :delete, class: "w-full font-medium text-sm px-3 py-2 rounded-lg text-primary bg-gray-200 hover:bg-gray-300" %> -
- <% elsif import.complete? && import.ai_processed? %>
<%= icon "check", class: "text-success" %> diff --git a/config/locales/views/imports/en.yml b/config/locales/views/imports/en.yml index 827fd2e76..8e0904ca3 100644 --- a/config/locales/views/imports/en.yml +++ b/config/locales/views/imports/en.yml @@ -253,11 +253,13 @@ en: back_to_imports: Back to imports reconciliation_title: Reconciliation report reconciliation_description: We've compared your statement against synced data and everything matches. + reconciliation_mismatch_description: We've compared your statement against synced data and found discrepancies. Review the information below and adjust as needed. reconciliation_details_label: Reconciliation Details reconciliation_statement_balance: "Statement closing balance" reconciliation_synced_balance: "Synced closing balance" reconciliation_balance_match: Balance match reconciliation_balance_yes: Yes + reconciliation_balance_no: No reconciliation_statement_txns: Statement transactions reconciliation_synced_txns: Synced transactions reconciliation_matched_txns: Matched transactions