""" Transactions API Blueprint This module contains API endpoints for transaction management. Handles CRUD operations for financial transactions with validation and filtering. """ from flask import Blueprint, request, jsonify from marshmallow import ValidationError from sqlalchemy.exc import IntegrityError from datetime import datetime, date from app.models import Transaction, Category from app.schemas import ( TransactionCreateSchema, TransactionUpdateSchema, TransactionListSchema, TransactionFilterSchema ) from app.extensions import db # Create transactions blueprint transactions_bp = Blueprint('transactions', __name__) # Initialize schemas transaction_create_schema = TransactionCreateSchema() transaction_update_schema = TransactionUpdateSchema() transaction_list_schema = TransactionListSchema(many=True) transaction_detail_schema = TransactionListSchema() transaction_filter_schema = TransactionFilterSchema() @transactions_bp.route('/', methods=['GET']) def get_transactions(): """ Get all transactions with optional filtering. Query Parameters: - type: string (income/expense) - Filter by transaction type - category_id: integer - Filter by category ID - start_date: string (YYYY-MM-DD) - Filter from date - end_date: string (YYYY-MM-DD) - Filter to date - page: integer (default: 1) - Page number for pagination - per_page: integer (default: 20, max: 100) - Items per page Returns: - 200: List of transactions with pagination info """ try: # Validate filter parameters filter_data = transaction_filter_schema.load(request.args.to_dict()) # Start with base query query = Transaction.query # Apply filters if filter_data.get('type'): query = query.filter(Transaction.type == filter_data['type']) if filter_data.get('category_id'): query = query.filter(Transaction.category_id == filter_data['category_id']) if filter_data.get('start_date'): query = query.filter(Transaction.transaction_date >= filter_data['start_date']) if filter_data.get('end_date'): query = query.filter(Transaction.transaction_date <= filter_data['end_date']) # Order by transaction_date descending, then by created_at descending query = query.order_by(Transaction.transaction_date.desc(), Transaction.created_at.desc()) # Pagination page = filter_data['page'] per_page = filter_data['per_page'] paginated = query.paginate( page=page, per_page=per_page, error_out=False ) # Serialize transactions with category names transactions_data = [] for transaction in paginated.items: data = transaction_detail_schema.dump(transaction) if transaction.category: data['category_name'] = transaction.category.name transactions_data.append(data) return jsonify({ 'success': True, 'data': transactions_data, 'pagination': { 'page': page, 'per_page': per_page, 'total': paginated.total, 'pages': paginated.pages, 'has_next': paginated.has_next, 'has_prev': paginated.has_prev } }), 200 except ValidationError as e: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'Filter validation failed', 'details': e.messages }), 400 except Exception as e: return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to retrieve transactions' }), 500 @transactions_bp.route('/', methods=['POST']) def create_transaction(): """ Create a new transaction. Request Body: - description: string (required) - Transaction description - amount: number (required) - Transaction amount (max 2 decimal places) - type: string (required) - 'income' or 'expense' - category_id: integer (optional) - Category ID - transaction_date: string (optional, default: today) - Date in YYYY-MM-DD format - notes: string (optional) - Additional notes Returns: - 201: Created transaction - 400: Validation error - 404: Category not found """ try: # Validate and deserialize request data json_data = request.get_json() if not json_data: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'No JSON data provided' }), 400 validated_data = transaction_create_schema.load(json_data) # Validate category exists if provided if validated_data.get('category_id'): category = Category.query.get(validated_data['category_id']) if not category: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Category with ID {validated_data["category_id"]} not found' }), 404 if not category.is_active: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'Cannot assign transaction to inactive category' }), 400 # Create new transaction transaction = Transaction(**validated_data) db.session.add(transaction) db.session.commit() # Return created transaction with category name data = transaction_detail_schema.dump(transaction) if transaction.category: data['category_name'] = transaction.category.name return jsonify({ 'success': True, 'data': data, 'message': 'Transaction created successfully' }), 201 except ValidationError as e: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'Input validation failed', 'details': e.messages }), 400 except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to create transaction' }), 500 @transactions_bp.route('/', methods=['GET']) def get_transaction(transaction_id): """ Get a specific transaction by ID. Path Parameters: - transaction_id: integer - Transaction ID Returns: - 200: Transaction details with category information - 404: Transaction not found """ try: transaction = Transaction.query.get(transaction_id) if not transaction: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Transaction with ID {transaction_id} not found' }), 404 # Serialize transaction with category name data = transaction_detail_schema.dump(transaction) if transaction.category: data['category_name'] = transaction.category.name return jsonify({ 'success': True, 'data': data }), 200 except Exception as e: return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to retrieve transaction' }), 500 @transactions_bp.route('/', methods=['PUT']) def update_transaction(transaction_id): """ Update a specific transaction. Path Parameters: - transaction_id: integer - Transaction ID Request Body: - description: string (optional) - Transaction description - amount: number (optional) - Transaction amount - type: string (optional) - 'income' or 'expense' - category_id: integer (optional) - Category ID - transaction_date: string (optional) - Date in YYYY-MM-DD format - notes: string (optional) - Additional notes Returns: - 200: Updated transaction - 400: Validation error - 404: Transaction or category not found """ try: transaction = Transaction.query.get(transaction_id) if not transaction: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Transaction with ID {transaction_id} not found' }), 404 # Validate and deserialize request data json_data = request.get_json() if not json_data: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'No JSON data provided' }), 400 validated_data = transaction_update_schema.load(json_data) # Validate category exists if being updated if 'category_id' in validated_data and validated_data['category_id']: category = Category.query.get(validated_data['category_id']) if not category: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Category with ID {validated_data["category_id"]} not found' }), 404 if not category.is_active: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'Cannot assign transaction to inactive category' }), 400 # Update transaction fields for field, value in validated_data.items(): setattr(transaction, field, value) db.session.commit() # Return updated transaction with category name data = transaction_detail_schema.dump(transaction) if transaction.category: data['category_name'] = transaction.category.name return jsonify({ 'success': True, 'data': data, 'message': 'Transaction updated successfully' }), 200 except ValidationError as e: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'Input validation failed', 'details': e.messages }), 400 except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to update transaction' }), 500 @transactions_bp.route('/', methods=['DELETE']) def delete_transaction(transaction_id): """ Delete a specific transaction. Path Parameters: - transaction_id: integer - Transaction ID Returns: - 200: Transaction deleted successfully - 404: Transaction not found """ try: transaction = Transaction.query.get(transaction_id) if not transaction: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Transaction with ID {transaction_id} not found' }), 404 # Hard delete the transaction db.session.delete(transaction) db.session.commit() return jsonify({ 'success': True, 'message': 'Transaction deleted successfully' }), 200 except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to delete transaction' }), 500