mirror of
https://github.com/we-promise/sure.git
synced 2026-04-12 16:47:22 +00:00
* Initial implementation * Tiingo fixes * Adds 2 providers, remove 2 * Add extra checks * FIX a big hotwire race condition // Fix hotwire_combobox race condition: when typing quickly, a slow response for // an early query (e.g. "A") can overwrite the correct results for the final query // (e.g. "AAPL"). We abort the previous in-flight request whenever a new one fires, // so stale Turbo Stream responses never reach the DOM. * pipelock * Update price_test.rb * Reviews * i8n * fixes * fixes * Update tiingo.rb * fixes * Improvements * Big revamp * optimisations * Update 20260408151837_add_offline_reason_to_securities.rb * Add missing tests, fixes * small rank tests * FIX tests * Update show.html.erb * Update resolver.rb * Update usd_converter.rb * Update holdings_controller.rb * Update holdings_controller.rb * Update holdings_controller.rb * Update holdings_controller.rb * Update holdings_controller.rb * Update _yahoo_finance_settings.html.erb
189 lines
5.3 KiB
Ruby
189 lines
5.3 KiB
Ruby
require "csv"
|
|
|
|
class Assistant::Function::ImportBankStatement < Assistant::Function
|
|
class << self
|
|
def name
|
|
"import_bank_statement"
|
|
end
|
|
|
|
def description
|
|
<<~INSTRUCTIONS
|
|
Use this to import transactions from a bank statement PDF that has already been uploaded.
|
|
|
|
This function will:
|
|
1. Extract transaction data from the PDF using AI
|
|
2. Create a transaction import with the extracted data
|
|
3. Return the import ID and extracted transactions for review
|
|
|
|
The PDF must have already been uploaded via the PDF import feature.
|
|
Only use this for PDFs that are identified as bank statements.
|
|
|
|
Example:
|
|
|
|
```
|
|
import_bank_statement({
|
|
pdf_import_id: "abc123-def456",
|
|
account_id: "xyz789"
|
|
})
|
|
```
|
|
|
|
If account_id is not provided, you should ask the user which account to import to.
|
|
INSTRUCTIONS
|
|
end
|
|
end
|
|
|
|
def strict_mode?
|
|
false
|
|
end
|
|
|
|
def params_schema
|
|
build_schema(
|
|
required: [ "pdf_import_id" ],
|
|
properties: {
|
|
pdf_import_id: {
|
|
type: "string",
|
|
description: "The ID of the PDF import to extract transactions from"
|
|
},
|
|
account_id: {
|
|
type: "string",
|
|
description: "The ID of the account to import transactions into. If not provided, will return available accounts."
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
def call(params = {})
|
|
pdf_import = family.imports.find_by(id: params["pdf_import_id"], type: "PdfImport")
|
|
|
|
unless pdf_import
|
|
return {
|
|
success: false,
|
|
error: "PDF import not found",
|
|
message: "Could not find a PDF import with ID: #{params["pdf_import_id"]}"
|
|
}
|
|
end
|
|
|
|
unless pdf_import.document_type == "bank_statement"
|
|
return {
|
|
success: false,
|
|
error: "not_bank_statement",
|
|
message: "This PDF is not a bank statement. Document type: #{pdf_import.document_type}",
|
|
available_actions: [ "Use a different PDF that is a bank statement" ]
|
|
}
|
|
end
|
|
|
|
# If no account specified, return available accounts
|
|
if params["account_id"].blank?
|
|
return {
|
|
success: false,
|
|
error: "account_required",
|
|
message: "Please specify which account to import transactions into",
|
|
available_accounts: family.accounts.visible.depository.map { |a| { id: a.id, name: a.name } }
|
|
}
|
|
end
|
|
|
|
account = family.accounts.find_by(id: params["account_id"])
|
|
unless account
|
|
return {
|
|
success: false,
|
|
error: "account_not_found",
|
|
message: "Account not found",
|
|
available_accounts: family.accounts.visible.depository.map { |a| { id: a.id, name: a.name } }
|
|
}
|
|
end
|
|
|
|
# Extract transactions from the PDF using provider
|
|
provider = Provider::Registry.get_provider(:openai)
|
|
unless provider
|
|
return {
|
|
success: false,
|
|
error: "provider_not_configured",
|
|
message: "OpenAI provider is not configured"
|
|
}
|
|
end
|
|
|
|
response = provider.extract_bank_statement(
|
|
pdf_content: pdf_import.pdf_file_content,
|
|
model: openai_model,
|
|
family: family
|
|
)
|
|
|
|
unless response.success?
|
|
error_message = response.error&.message || "Unknown extraction error"
|
|
return {
|
|
success: false,
|
|
error: "extraction_failed",
|
|
message: "Failed to extract transactions: #{error_message}"
|
|
}
|
|
end
|
|
|
|
result = response.data
|
|
|
|
if result[:transactions].blank?
|
|
return {
|
|
success: false,
|
|
error: "no_transactions_found",
|
|
message: "Could not extract any transactions from the bank statement"
|
|
}
|
|
end
|
|
|
|
# Create a CSV from extracted transactions
|
|
csv_content = generate_csv(result[:transactions])
|
|
|
|
# Create a TransactionImport
|
|
import = family.imports.create!(
|
|
type: "TransactionImport",
|
|
account: account,
|
|
raw_file_str: csv_content,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
category_col_label: "category",
|
|
notes_col_label: "notes",
|
|
date_format: "%Y-%m-%d",
|
|
signage_convention: "inflows_positive"
|
|
)
|
|
|
|
import.generate_rows_from_csv
|
|
|
|
{
|
|
success: true,
|
|
import_id: import.id,
|
|
transaction_count: result[:transactions].size,
|
|
transactions_preview: result[:transactions].first(5),
|
|
statement_period: result[:period],
|
|
account_holder: result[:account_holder],
|
|
message: "Successfully extracted #{result[:transactions].size} transactions. Import created with ID: #{import.id}. Review and publish when ready."
|
|
}
|
|
rescue Provider::Error, Faraday::Error, Timeout::Error, RuntimeError => e
|
|
Rails.logger.error("ImportBankStatement error: #{e.class.name} - #{e.message}")
|
|
Rails.logger.error(e.backtrace.first(10).join("\n"))
|
|
{
|
|
success: false,
|
|
error: "extraction_failed",
|
|
message: "Failed to extract transactions: #{e.message.truncate(200)}"
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def generate_csv(transactions)
|
|
CSV.generate do |csv|
|
|
csv << %w[date amount name category notes]
|
|
transactions.each do |txn|
|
|
csv << [
|
|
txn[:date],
|
|
txn[:amount],
|
|
txn[:name] || txn[:description],
|
|
txn[:category],
|
|
txn[:notes]
|
|
]
|
|
end
|
|
end
|
|
end
|
|
|
|
def openai_model
|
|
ENV["OPENAI_MODEL"].presence || Provider::Openai::DEFAULT_MODEL
|
|
end
|
|
end
|