"""Portfolio management — group companies into collections for PE-backed portfolio management."""
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from app.models import db, Portfolio, Company, portfolio_companies
from app.utils.csrf import require_csrf
from app.utils.pagination import paginate_query
portfolios_bp = Blueprint('portfolios', __name__, url_prefix='/api/portfolios')
def _require_super_admin():
"""Check that current user is super_admin. Returns error tuple or None."""
if not current_user.is_authenticated:
return jsonify({'error': 'Authentication required'}), 401
if current_user.role != 'super_admin':
return jsonify({'error': 'Admin access required'}), 403
return None
# =============================================================================
# Portfolio CRUD
# =============================================================================
@portfolios_bp.route('/', methods=['GET'])
@login_required
def list_portfolios():
"""List all portfolios."""
err = _require_super_admin()
if err:
return err
portfolios = Portfolio.query.order_by(Portfolio.created_at.desc())
pagination, meta = paginate_query(portfolios)
items = [p.to_dict() for p in pagination.items]
return jsonify({
'items': items,
**meta,
})
@portfolios_bp.route('/', methods=['POST'])
@login_required
@require_csrf
def create_portfolio():
"""Create a new portfolio."""
err = _require_super_admin()
if err:
return err
data = request.get_json()
if not data or not data.get('name'):
return jsonify({'error': 'Name is required'}), 400
portfolio = Portfolio(
name=data['name'],
description=data.get('description', ''),
created_by=current_user.id,
)
db.session.add(portfolio)
# Add companies if provided
company_ids = data.get('company_ids', [])
if company_ids:
companies = Company.query.filter(Company.id.in_(company_ids)).all()
portfolio.companies = companies
db.session.commit()
return jsonify({'portfolio': portfolio.to_dict(), 'message': 'Portfolio created'}), 201
@portfolios_bp.route('/<portfolio_id>', methods=['GET'])
@login_required
def get_portfolio(portfolio_id):
"""Get portfolio detail."""
err = _require_super_admin()
if err:
return err
portfolio = db.session.get(Portfolio, portfolio_id)
if not portfolio:
return jsonify({'error': 'Portfolio not found'}), 404
return jsonify({'portfolio': portfolio.to_dict()})
@portfolios_bp.route('/<portfolio_id>', methods=['PUT'])
@login_required
@require_csrf
def update_portfolio(portfolio_id):
"""Update portfolio name/description."""
err = _require_super_admin()
if err:
return err
portfolio = db.session.get(Portfolio, portfolio_id)
if not portfolio:
return jsonify({'error': 'Portfolio not found'}), 404
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
if 'name' in data:
portfolio.name = data['name']
if 'description' in data:
portfolio.description = data['description']
db.session.commit()
return jsonify({'portfolio': portfolio.to_dict(), 'message': 'Portfolio updated'})
@portfolios_bp.route('/<portfolio_id>', methods=['DELETE'])
@login_required
@require_csrf
def delete_portfolio(portfolio_id):
"""Delete a portfolio."""
err = _require_super_admin()
if err:
return err
portfolio = db.session.get(Portfolio, portfolio_id)
if not portfolio:
return jsonify({'error': 'Portfolio not found'}), 404
db.session.delete(portfolio)
db.session.commit()
return jsonify({'message': 'Portfolio deleted'})
# =============================================================================
# Portfolio Company management
# =============================================================================
@portfolios_bp.route('/<portfolio_id>/companies', methods=['POST'])
@login_required
@require_csrf
def add_companies(portfolio_id):
"""Add companies to a portfolio."""
err = _require_super_admin()
if err:
return err
portfolio = db.session.get(Portfolio, portfolio_id)
if not portfolio:
return jsonify({'error': 'Portfolio not found'}), 404
data = request.get_json()
if not data or not data.get('company_ids'):
return jsonify({'error': 'company_ids is required'}), 400
company_ids = data['company_ids']
companies = Company.query.filter(Company.id.in_(company_ids)).all()
added = []
for company in companies:
if company not in portfolio.companies:
portfolio.companies.append(company)
added.append(company.id)
db.session.commit()
return jsonify({
'portfolio': portfolio.to_dict(),
'added': added,
'message': f'{len(added)} company(ies) added'
})
@portfolios_bp.route('/<portfolio_id>/companies', methods=['DELETE'])
@login_required
@require_csrf
def remove_companies(portfolio_id):
"""Remove companies from a portfolio."""
err = _require_super_admin()
if err:
return err
portfolio = db.session.get(Portfolio, portfolio_id)
if not portfolio:
return jsonify({'error': 'Portfolio not found'}), 404
data = request.get_json()
if not data or not data.get('company_ids'):
return jsonify({'error': 'company_ids is required'}), 400
company_ids = data['company_ids']
removed = []
for company in portfolio.companies:
if company.id in company_ids:
portfolio.companies.remove(company)
removed.append(company.id)
db.session.commit()
return jsonify({
'portfolio': portfolio.to_dict(),
'removed': removed,
'message': f'{len(removed)} company(ies) removed'
})