mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
194 lines
4.4 KiB
Ruby
194 lines
4.4 KiB
Ruby
class PdfImport < Import
|
|
has_one_attached :pdf_file, dependent: :purge_later
|
|
|
|
validates :document_type, inclusion: { in: DOCUMENT_TYPES }, allow_nil: true
|
|
|
|
def import!
|
|
raise "Account required for PDF import" unless account.present?
|
|
|
|
transaction do
|
|
mappings.each(&:create_mappable!)
|
|
|
|
new_transactions = rows.map do |row|
|
|
category = mappings.categories.mappable_for(row.category)
|
|
|
|
Transaction.new(
|
|
category: category,
|
|
entry: Entry.new(
|
|
account: account,
|
|
date: row.date_iso,
|
|
amount: row.signed_amount,
|
|
name: row.name,
|
|
currency: row.currency,
|
|
notes: row.notes,
|
|
import: self,
|
|
import_locked: true
|
|
)
|
|
)
|
|
end
|
|
|
|
Transaction.import!(new_transactions, recursive: true) if new_transactions.any?
|
|
end
|
|
end
|
|
|
|
def pdf_uploaded?
|
|
pdf_file.attached?
|
|
end
|
|
|
|
def ai_processed?
|
|
ai_summary.present?
|
|
end
|
|
|
|
def process_with_ai_later
|
|
ProcessPdfJob.perform_later(self)
|
|
end
|
|
|
|
def process_with_ai
|
|
provider = Provider::Registry.get_provider(:openai)
|
|
raise "AI provider not configured" unless provider
|
|
raise "AI provider does not support PDF processing" unless provider.supports_pdf_processing?
|
|
|
|
response = provider.process_pdf(
|
|
pdf_content: pdf_file_content,
|
|
family: family
|
|
)
|
|
|
|
unless response.success?
|
|
error_message = response.error&.message || "Unknown PDF processing error"
|
|
raise error_message
|
|
end
|
|
|
|
result = response.data
|
|
update!(
|
|
ai_summary: result.summary,
|
|
document_type: result.document_type
|
|
)
|
|
|
|
result
|
|
end
|
|
|
|
def extract_transactions
|
|
return unless statement_with_transactions?
|
|
|
|
provider = Provider::Registry.get_provider(:openai)
|
|
raise "AI provider not configured" unless provider
|
|
|
|
response = provider.extract_bank_statement(
|
|
pdf_content: pdf_file_content,
|
|
family: family
|
|
)
|
|
|
|
unless response.success?
|
|
error_message = response.error&.message || "Unknown extraction error"
|
|
raise error_message
|
|
end
|
|
|
|
update!(extracted_data: response.data)
|
|
response.data
|
|
end
|
|
|
|
def bank_statement?
|
|
document_type == "bank_statement"
|
|
end
|
|
|
|
def statement_with_transactions?
|
|
document_type.in?(%w[bank_statement credit_card_statement])
|
|
end
|
|
|
|
def has_extracted_transactions?
|
|
extracted_data.present? && extracted_data["transactions"].present?
|
|
end
|
|
|
|
def extracted_transactions
|
|
extracted_data&.dig("transactions") || []
|
|
end
|
|
|
|
def generate_rows_from_extracted_data
|
|
transaction do
|
|
rows.destroy_all
|
|
|
|
unless has_extracted_transactions?
|
|
update_column(:rows_count, 0)
|
|
return
|
|
end
|
|
|
|
currency = account&.currency || family.currency
|
|
|
|
mapped_rows = extracted_transactions.map do |txn|
|
|
{
|
|
import_id: id,
|
|
date: format_date_for_import(txn["date"]),
|
|
amount: txn["amount"].to_s,
|
|
name: txn["name"].to_s,
|
|
category: txn["category"].to_s,
|
|
notes: txn["notes"].to_s,
|
|
currency: currency
|
|
}
|
|
end
|
|
|
|
Import::Row.insert_all!(mapped_rows) if mapped_rows.any?
|
|
update_column(:rows_count, mapped_rows.size)
|
|
end
|
|
end
|
|
|
|
def send_next_steps_email(user)
|
|
PdfImportMailer.with(
|
|
user: user,
|
|
pdf_import: self
|
|
).next_steps.deliver_later
|
|
end
|
|
|
|
def uploaded?
|
|
pdf_uploaded?
|
|
end
|
|
|
|
def configured?
|
|
ai_processed? && rows_count > 0
|
|
end
|
|
|
|
def cleaned?
|
|
configured? && rows.all?(&:valid?)
|
|
end
|
|
|
|
def publishable?
|
|
account.present? && statement_with_transactions? && cleaned? && mappings.all?(&:valid?)
|
|
end
|
|
|
|
def column_keys
|
|
%i[date amount name category notes]
|
|
end
|
|
|
|
def requires_csv_workflow?
|
|
false
|
|
end
|
|
|
|
def pdf_file_content
|
|
return nil unless pdf_file.attached?
|
|
|
|
pdf_file.download
|
|
end
|
|
|
|
def required_column_keys
|
|
%i[date amount]
|
|
end
|
|
|
|
def mapping_steps
|
|
base = []
|
|
# Only include CategoryMapping if rows have non-empty categories
|
|
base << Import::CategoryMapping if rows.where.not(category: [ nil, "" ]).exists?
|
|
# Note: PDF imports use direct account selection in the UI, not AccountMapping
|
|
# AccountMapping is designed for CSV imports where rows have different account values
|
|
base
|
|
end
|
|
|
|
private
|
|
|
|
def format_date_for_import(date_str)
|
|
return "" if date_str.blank?
|
|
|
|
Date.parse(date_str).strftime(date_format)
|
|
rescue ArgumentError
|
|
date_str.to_s
|
|
end
|
|
end
|