persona-finance-tracker-demo/app/models/transaction.py
2025-09-13 17:40:23 +05:30

112 lines
4.1 KiB
Python

"""
Transaction Model
SQLAlchemy model for financial transactions.
Handles income and expense records with precise decimal arithmetic.
"""
from datetime import datetime, date
from decimal import Decimal
from app.extensions import db
class Transaction(db.Model):
"""
Transaction model for financial records.
Attributes:
id (int): Primary key
description (str): Transaction description
amount (Decimal): Transaction amount (precise decimal)
type (str): Transaction type ('income' or 'expense')
category_id (int): Foreign key to Category
transaction_date (date): Date of transaction
notes (str): Optional additional notes
created_at (datetime): Creation timestamp
"""
__tablename__ = 'transactions'
# Primary key
id = db.Column(db.Integer, primary_key=True)
# Transaction information
description = db.Column(db.String(255), nullable=False)
amount = db.Column(db.Numeric(10, 2), nullable=False) # DECIMAL(10,2) for precise money handling
type = db.Column(db.String(10), nullable=False) # 'income' or 'expense'
notes = db.Column(db.Text)
# Relationships
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
# Dates
transaction_date = db.Column(db.Date, nullable=False, default=date.today)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# Database constraints
__table_args__ = (
db.CheckConstraint('amount > 0', name='check_amount_positive'),
db.CheckConstraint("type IN ('income', 'expense')", name='check_type_valid'),
)
def __repr__(self):
"""String representation of Transaction."""
return f'<Transaction {self.type}: {self.amount} - {self.description}>'
def to_dict(self):
"""Convert transaction to dictionary for JSON serialization."""
return {
'id': self.id,
'description': self.description,
'amount': float(self.amount) if self.amount else 0,
'type': self.type,
'category_id': self.category_id,
'category_name': self.category.name if self.category else None,
'transaction_date': self.transaction_date.isoformat() if self.transaction_date else None,
'notes': self.notes,
'created_at': self.created_at.isoformat() if self.created_at else None
}
@property
def amount_decimal(self):
"""Get amount as Decimal object for precise calculations."""
return Decimal(str(self.amount)) if self.amount else Decimal('0')
@classmethod
def get_by_type(cls, transaction_type):
"""Get transactions by type (income/expense)."""
return cls.query.filter_by(type=transaction_type).all()
@classmethod
def get_by_date_range(cls, start_date, end_date):
"""Get transactions within date range."""
return cls.query.filter(
cls.transaction_date >= start_date,
cls.transaction_date <= end_date
).all()
@classmethod
def get_by_category(cls, category_id):
"""Get transactions by category."""
return cls.query.filter_by(category_id=category_id).all()
@classmethod
def calculate_total_by_type(cls, transaction_type):
"""Calculate total amount for a transaction type."""
result = db.session.query(db.func.sum(cls.amount)).filter_by(type=transaction_type).scalar()
return Decimal(str(result)) if result else Decimal('0')
@classmethod
def get_summary_stats(cls):
"""Get summary statistics for all transactions."""
total_income = cls.calculate_total_by_type('income')
total_expenses = cls.calculate_total_by_type('expense')
net_balance = total_income - total_expenses
transaction_count = cls.query.count()
return {
'total_income': float(total_income),
'total_expenses': float(total_expenses),
'net_balance': float(net_balance),
'transaction_count': transaction_count
}