mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
* Use dependent: :purge_later for user profile_image cleanup This is a simpler alternative to PR #787's callback-based approach. Instead of adding a custom callback and method, we use Rails' built-in `dependent: :purge_later` option which is already used by FamilyExport and other models in the codebase. This single-line change ensures orphaned ActiveStorage attachments are automatically purged when a user is destroyed, without the overhead of querying all attachments manually. https://claude.ai/code/session_01Np3deHEAJqCBfz3aY7c3Tk * Add dependent: :purge_later to all ActiveStorage attachments Extends the attachment cleanup from PR #787 to cover ALL models with ActiveStorage attachments, not just User.profile_image. Models updated: - PdfImport.pdf_file - prevents orphaned PDF files from imports - Account.logo - prevents orphaned account logos - PlaidItem.logo, SimplefinItem.logo, SnaptradeItem.logo, CoinstatsItem.logo, CoinbaseItem.logo, LunchflowItem.logo, MercuryItem.logo, EnableBankingItem.logo - prevents orphaned provider logos This ensures that when a family is deleted (cascade from last user purge), all associated storage files are properly cleaned up via Rails' built-in dependent: :purge_later mechanism. https://claude.ai/code/session_01Np3deHEAJqCBfz3aY7c3Tk * Make sure `Provider` generator adds it * Fix tests --------- Co-authored-by: Claude <noreply@anthropic.com>
190 lines
4.3 KiB
Ruby
190 lines
4.3 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 bank_statement?
|
|
|
|
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 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? && bank_statement? && 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
|