""" Categories API Blueprint This module contains API endpoints for category management. Handles CRUD operations for transaction categories with full validation. """ from flask import Blueprint, request, jsonify from marshmallow import ValidationError from sqlalchemy.exc import IntegrityError from typing import Dict, Any from app.models import Category from app.schemas import CategoryCreateSchema, CategoryUpdateSchema, CategoryListSchema from app.extensions import db # Create categories blueprint categories_bp = Blueprint('categories', __name__) # Initialize schemas category_create_schema = CategoryCreateSchema() category_update_schema = CategoryUpdateSchema() category_list_schema = CategoryListSchema(many=True) category_detail_schema = CategoryListSchema() @categories_bp.route('/', methods=['GET']) def get_categories(): """ Get all categories with optional transaction counts. Query Parameters: - include_counts: boolean (default: false) - Include transaction counts - active_only: boolean (default: true) - Show only active categories Returns: - 200: List of categories """ try: # Parse query parameters include_counts = request.args.get('include_counts', 'false').lower() == 'true' active_only = request.args.get('active_only', 'true').lower() == 'true' if include_counts: # Use efficient query method to get categories with counts categories_with_counts = Category.get_categories_with_counts() # Filter by active status if requested if active_only: categories_with_counts = [ (cat, count) for cat, count in categories_with_counts if cat.is_active ] # Format response with counts data = [] for category, count in categories_with_counts: cat_dict = category_detail_schema.dump(category) cat_dict['transaction_count'] = count data.append(cat_dict) else: # Regular query without counts if active_only: categories = Category.get_active_categories() else: categories = Category.query.all() data = category_list_schema.dump(categories) return jsonify({ 'success': True, 'data': data, 'count': len(data) }), 200 except Exception as e: return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to retrieve categories' }), 500 @categories_bp.route('/', methods=['POST']) def create_category(): """ Create a new category. Request Body: - name: string (required) - Category name (unique) - description: string (optional) - Category description - color: string (optional) - Hex color code - is_active: boolean (optional, default: true) Returns: - 201: Created category - 400: Validation error - 409: Category name already exists """ 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 = category_create_schema.load(json_data) # Check if category name already exists existing_category = Category.find_by_name(validated_data['name']) if existing_category: return jsonify({ 'success': False, 'error': 'conflict', 'message': f'Category with name "{validated_data["name"]}" already exists' }), 409 # Create new category category = Category(**validated_data) db.session.add(category) db.session.commit() # Return created category return jsonify({ 'success': True, 'data': category_detail_schema.dump(category), 'message': 'Category created successfully' }), 201 except ValidationError as e: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'Input validation failed', 'details': e.messages }), 400 except IntegrityError: db.session.rollback() return jsonify({ 'success': False, 'error': 'conflict', 'message': 'Category name must be unique' }), 409 except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to create category' }), 500 @categories_bp.route('/', methods=['GET']) def get_category(category_id): """ Get a specific category by ID. Path Parameters: - category_id: integer - Category ID Returns: - 200: Category details - 404: Category not found """ try: category = Category.query.get(category_id) if not category: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Category with ID {category_id} not found' }), 404 # Get transaction count for this category transaction_count = category.get_transaction_count() data = category_detail_schema.dump(category) data['transaction_count'] = transaction_count return jsonify({ 'success': True, 'data': data }), 200 except Exception as e: return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to retrieve category' }), 500 @categories_bp.route('/', methods=['PUT']) def update_category(category_id): """ Update a specific category. Path Parameters: - category_id: integer - Category ID Request Body: - name: string (optional) - Category name - description: string (optional) - Category description - color: string (optional) - Hex color code - is_active: boolean (optional) Returns: - 200: Updated category - 400: Validation error - 404: Category not found - 409: Category name already exists """ try: category = Category.query.get(category_id) if not category: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Category with ID {category_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 = category_update_schema.load(json_data) # Check if name is being changed and if new name already exists if 'name' in validated_data and validated_data['name'] != category.name: existing_category = Category.find_by_name(validated_data['name']) if existing_category: return jsonify({ 'success': False, 'error': 'conflict', 'message': f'Category with name "{validated_data["name"]}" already exists' }), 409 # Update category fields for field, value in validated_data.items(): setattr(category, field, value) db.session.commit() return jsonify({ 'success': True, 'data': category_detail_schema.dump(category), 'message': 'Category updated successfully' }), 200 except ValidationError as e: return jsonify({ 'success': False, 'error': 'validation_failed', 'message': 'Input validation failed', 'details': e.messages }), 400 except IntegrityError: db.session.rollback() return jsonify({ 'success': False, 'error': 'conflict', 'message': 'Category name must be unique' }), 409 except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to update category' }), 500 @categories_bp.route('/', methods=['DELETE']) def delete_category(category_id): """ Soft delete a category (set is_active = False). Path Parameters: - category_id: integer - Category ID Returns: - 200: Category deleted successfully - 404: Category not found - 409: Cannot delete category with transactions """ try: category = Category.query.get(category_id) if not category: return jsonify({ 'success': False, 'error': 'not_found', 'message': f'Category with ID {category_id} not found' }), 404 # Check if category has active transactions transaction_count = category.get_transaction_count() if transaction_count > 0: return jsonify({ 'success': False, 'error': 'conflict', 'message': f'Cannot delete category with {transaction_count} transactions. Please reassign or delete transactions first.' }), 409 # Soft delete by setting is_active to False category.is_active = False db.session.commit() return jsonify({ 'success': True, 'message': 'Category deleted successfully' }), 200 except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'error': 'internal_server_error', 'message': 'Failed to delete category' }), 500