331 lines
10 KiB
Python
331 lines
10 KiB
Python
"""
|
|
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('/<int:category_id>', 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('/<int:category_id>', 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('/<int:category_id>', 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
|